Новый перевод от команды Spring АйО расскажет вам, как грамотно использовать кеширование контекста для сокращения времени сборки приложения и как избежать часто встречающихся ловушек, в которые попадают многие программисты, когда делают это неправильно.
Ничто не утомляет так, как длинные циклы обратной связи, вызванные медленными сборками. Я недавно посвятил один день улучшению настроек интеграционных тестов для большого проекта. Результат ошеломил: время сборки (при запуске mvn clean verify
) уменьшилось с 25 до 5 минут просто благодаря тому, что я разобрался с настройками тестов. Это стало возможно за счет использования по максимуму механизма кеширования контекста от Spring Test. Данная статья расскажет вам о механизме кеширования контекста, включая рекомендации относительно того, как выжать максимум из этой функциональности, чтобы резко сократить время сборки.
Механизм кеширования контекста в Spring Test
Каждый раз, когда вы выходите за рамки тривиального юнит-тестирования и включаете возможности Spring Test, вашим тестам понадобится работающий Spring Context (например, @SpringBootTest
, @WebMvcTest
, @DataJpaTest
). Отличная поддержка тестов от Spring в комбинации со Spring Boot позволяет создавать sliced контекст приложения, который соответствует цели вашего тестирования (например, можно включить только бины, относящиеся к веб-слою, при использовании @WebMvcTest
).
Запуск Spring Context для вашего теста занимает существенное время. Чаще всего это случается, когда вы поднимаете весь контекст, используя @SpringBootTest
. Это требует дополнительных временных ресурсов для выполнения вашего теста и может существенно увеличить общее время сборки, если каждый тест поднимает свой собственный контекст.
К счастью, Spring Test предоставляет механизм для кеширования уже поднятого контекста приложения и его повторного использования для дальнейших тестов с похожими потребностями в плане контекста.
Под капотом Spring для хранения различных контекстов приложения использует java.util.Map
. Поскольку не каждый контекст приложения может использоваться для всех тестов (представьте себе, каково это будет, использовать веб контекст приложения для тестирования persistence слоя), Spring Test создает ключ для идентификации уникальных настроек контекста на основании различных свойств/конфигурационных параметров:
locations
(из@ContextConfiguration
)classes
(часть@ContextConfiguration
)contextInitializerClasses
(из@ContextConfiguration
)contextCustomizers
(изContextCustomizerFactory
) – например,@DynamicPropertySource
,@MockBean
и@SpyBean
.contextLoader
(часть@ContextConfiguration
)parent
(из@ContextHierarchy
)activeProfiles
(пришедшее из@ActiveProfiles
)propertySourceLocations
(из@TestPropertySource
)propertySourceProperties
(из@TestPropertySource
)resourceBasePath
(часть@WebAppConfiguration
)
Каждый раз, когда меняется один из этих параметров, ключ контекста тоже поменяется. Если отсутствует запись в Map
`е для тестового контекста, который Spring собирается запустить (то есть случается сache miss), Spring Test создает новый контекст. При каждом cache hit Spring Test будет повторно использовать контекст и не будет запускать новый.
Если большинство ваших тестов делят между собой одну и ту же конфигурацию контекста, вы можете повторно использовать Spring контекст для нескольких тестов и таким образом снизить общее время сборки.
Но это в теории. Давайте посмотрим на то, что обычно идет не так или не дает этой крутой возможности Spring Test сиять во всей красе.
Подводные камни повторного использования контекста
Чаще всего использование @MockBean
или @SpyBean
не дает вам повторно использовать уже запущенный контекст приложения. Я часто видел, как использование этих аннотаций вызывает cache miss по кешу контекстов в тестовой функциональности вашего приложения.
Представьте себе, что приложение подписывается на JMS очередь, и вы хотите проверить ее обработку. Во время интеграционного теста вы помещаете сообщение в очередь (например, при использовании Testcontainers) и запускаете полный Spring контекст (@SpringBootTest
). Ленивый разработчик может замокать бины для логики обработки и всего лишь проверит, обработал ли получатель сообщение.
Некоторые разработчики могут возразить, что тестировать все сразу трудно и это требует большого предварительного сетапа. Этот аргумент имеет право на жизнь, но тогда попробуйте не запускать полный Spring контекст или спросите себя, действительно ли вы тестируете функциональность библиотеки.
Комментарий от команды Spring АйО
Использование @MockBean/@MockitoBean
или подобных аннотаций действительно может негативно повлиять на кеширование контекста Spring.
Однако использовать эти аннотации, при этом не теряя преимущество кеширования контекста, вполне себе можно. Данная статья не предусматривает такого примера. Позже редакция выпустит статью эксперта Spring АйО по этому вопросу, чтобы дополнить этот материал.
Другое потенциально опасное место можно обнаружить при тестировании веб слоя со @SpringBootTest
:
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class SecondApplicationTest {
@Autowired
private TestRestTemplate testRestTemplate;
@MockBean
private PersonService personService;
@Test
public void testPublicEndpoint() {
when(personService.getPerson()).thenReturn("testPerson");
String result = this.testRestTemplate
.getForObject("/", String.class);
assertEquals("testPerson", result);
}
}
Для приведенного выше теста может хватить MockMvc и @WebMvcTest
.
Другой ловушкой, в которую, по моему опыту, часто попадают люди, является чрезмерное использование @DirtiesContext
. Легитимные причины для использования этой аннотации существуют, например, когда вы модифицируете глобальное состояние приложения внутри теста. Но чаще всего это лишь признак лени разработчиков, натыкающихся на проблему при запуске всех тестов вместе. Обычно это происходит при тестировании частей, связанных с обменом сообщениями или базой данных.
Вместо того, чтобы потратить время на понимание первопричины проблемы, вы спасаете сами себя, используя @DirtiesContext
. Использование этой аннотации помечает Spring контекст как “грязный”, и Spring Test больше не будет его использовать. Как только кто-то введет эту аннотацию, скорее всего следующий разработчик примет её как данность, и уже никто не станет тратить время на исправление ситуации, так как “окно уже разбито”.
Более того, использование нескольких различных профилей для тестов с аннотацией @ActiveProfiles
также является распространенной ловушкой. Старайтесь избегать использования нескольких тестовых профилей (например, it
, it-web-test
, it-database
) или ограничьтесь минимально необходимым их количеством.
Давайте посмотрим на другие доступные техники, позволяющие извлечь максимальную выгоду из возможности повторного использования контекстов в Spring Test.
Приемы для эффективного переиспользования контекста
Говоря обобщенно, вам следует избегать любых изменений в глобальном состоянии контекста вашего приложения, которые могут воспрепятствовать повторному использованию этого контекста.
Убедитесь в том, что вы всегда производите очистку ресурсов и оставляете Spring Context чистым после каждого выполнения теста.
Комментарий от команды Spring АйО
На самом деле, если вы не занимаетесь тем, что
- Используете MutablePropertySource
или другие абстракции для регистрации свойств или профилей приложения
- Используете динамическую регистрацию компонентов или т.п. активности
То вы не "загрязняете" контекст. Здесь речь о том, что явно ничего для "очистки" контекста делать не надо.
Старайтесь насколько возможно придерживаться одинаковой конфигурации контекста для ваших интеграционных тестов. Чтобы достичь этого, вы можете либо ввести абстрактный родительский класс, который включает вашу общую конфигурацию:
@AutoConfigureMockMvc
@ContextConfiguration(initializers = CustomInitializer.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public abstract class AbstractIntegrationTest {
@BeforeAll
public static void commonSetup() {
// e.g. provide WireMock stubs
}
}
… либо написать кастомизированную аннотацию, которая включает все конфигурационные аннотации, например @FullIntegrationTest
.
Каждый раз, когда у вас возникает соблазн использовать @MockBean
для упрощения теста, подумайте ещё раз. Особенно если вы используете эту аннотацию в комбинации со @SpringBootTest
. Высока вероятность того, что Spring предоставляет аннотацию для запуска sliced контекста для той части вашего приложения, которую вы хотите протестировать. Более того, вы также можете подготовить кастомизированный контекст, в который войдут только те бины, которые вам необходимы.
Если вы в настоящее время распараллеливаете выполнение ваших интеграционных тестов, используя либо JUnit 5, либо плагин Failsafe, у Spring Test может не получиться использовать контексты повторно. Если ваши тесты не всегда запускаются в рамках одного и того же процесса, вы не сможете получить выгоду от механизма кеширования контекста. Попробуйте отключить параллелизацию и посмотреть, дает ли кеширование контекста больший выигрыш, чем параллельный запуск тестов.
Мысли на тему кеширования контекста в Spring Test
Если вам любопытно узнать, почему ваш текущий тест не использует повторно уже запущенный контекст, вы можете включить дебаг логирование:
logging.level.org.springframework.test.context.cache=DEBUG
Логи также покажут статистику по поводу кеширования контекста:
... Storing ApplicationContext [396485834] in cache under key
[[WebMergedContextConfiguration@222a59e6 testClass = ApplicationTests, locations = '{}', classes = '{class de.rieckpil.blog.Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', ... ]]
Размер кеша по умолчанию равен 32, но вы можете легко увеличить или уменьшить это значение:
spring.test.context.cache.maxSize=42
Всякий раз, когда достигается лимит на кеширование контекста, Spring Test использует политику исключения LRU (least recently used), чтобы освободить место под следующий контекст.
Приятного кеширования контекста со Spring Test.
Комментарий от команды Spring АйО
В процессе ревью данной статьи эксперты команды Spring АйО заинтересовались тем, чтобы донести более полную информацию по данной теме, поэтому совсем скоро вас ждет авторская статья.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.