Интеграционные тесты на Spring Boot могут тормозить разработку. Причина часто кроется в неэффективной работе с кэшем контекста Spring. 

Spring Test Profiler — это инструмент, который поможет выявить узкие места и оптимизировать конфигурацию тестов. В новом переводе от команды Spring АйО мы рассмотрим, какие тесты вызывают повторную загрузку контекста, где конфигурации расходятся и как можно унифицировать окружение для значительного ускорения тестов.


Ожидание прогона медленных интеграционных тестов является одной из самых раздражающих проблем в современной разработке на Spring Boot. Когда выполнение тестов занимает более 15 минут или они завершаются сбоем без видимой причины, это приводит к кризису в продуктивности, затрагивающему весь цикл разработки. Разработчики реже делают локальные прогоны тестов, чтобы избежать потери времени, что приводит к большому количеству багов в продакшене и разрушает непрерывную обратную связь, которая является основой эффективной agile-разработки.

Скрытая причина проблем с производительностью тестов Spring Boot

Основной источник проблем с производительностью тестов Spring Boot часто кроется в неправильной конфигурации тестовых контекстов. Механизм кэширования TestContext в Spring — мощный инструмент, созданный для ускорения выполнения тестов, — нередко используется неправильно, что даёт противоположный эффект. Понимание и оптимизация этого механизма может существенно сократить время сборки — в некоторых случаях ускорение достигает 50–80%.

Скрытая цена медленных тестов Spring Boot

Медленные тесты создают цепочку проблем, выходящих далеко за рамки простого неудобства.

Когда скорость прогона тестов становится узким местом, разработчики естественным образом адаптируют рабочие процессы, чтобы избежать этих задержек. В результате команды прибегают к различным обходным решениям: запускают только отдельные классы тестов, пропускают интеграционные тесты в процессе разработки или, что ещё хуже, коммитят код без локального запуска тестов.

Рассмотрим типичную ситуацию из реального проекта на Spring Boot:

$ ./mvnw verify
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.UserServiceTest
[INFO] Tests run: 15, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 8.429 s
[INFO] Running com.example.PaymentControllerTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.234 s
[INFO] Running com.example.OrderProcessingTest
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.891 s
[INFO] Running com.example.DatabaseIntegrationTest
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.567 s
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 18:23 min

Это количество времени на выполнение может показаться приемлемыми для отдельных классов тестов, но в совокупности они создают серьёзное препятствие для продуктивной разработки.

Каждая повторная загрузка контекста добавляет ненужные издержки, и без понимания происходящего «под капотом» мы не можем выявить возможности для оптимизации.

Корень проблемы, как правило, кроется в несогласованной конфигурации тестов в рамках всего набора. Когда тесты требуют немного отличающихся Spring-контекстов, фреймворк не может переиспользовать уже созданные контексты и вынужден создавать новые для каждого класса тестов. Этот процесс включает сканирование компонентов, создание бинов, внедрение зависимостей и, возможно, инициализацию базы данных — всё это требует значительных затрат времени и ресурсов.

Понимание кэширования в Spring TestContext

Фреймворк Spring TestContext включает механизм кэширования, разработанный для повышения производительности тестов путём повторного использования контекстов приложения между различными классами тестов. Если два теста используют идентичную конфигурацию контекста, Spring может переиспользовать один и тот же экземпляр ApplicationContext, избегая затратного процесса его создания.

Механизм кэширования работает на основе создания уникального ключа для каждой конфигурации контекста. Этот ключ формируется с учётом следующих факторов:

  • Классы конфигурации

  • Активные профили Spring

  • Источники свойств и их значения

  • Инициализаторы контекста

  • и т.д.

Когда тесту требуется контекст, Spring сначала проверяет, существует ли в кэше совместимый контекст.

Если подходящий контекст найден, он повторно используется.

Если нет — создаётся новый контекст, который затем сохраняется в кэше для последующего использования.

Рассмотрим, как различные конфигурации тестов влияют на кэширование контекста:

@SpringBootTest
@TestPropertySource(properties = "logging.level.com.example=DEBUG")
class UserServiceIT {

  @Autowired
  private UserService userService;

  @Test
  void testCreateUser() {
    User user = userService.createUser("john.doe", "john@example.com");
    assertThat(user.getUsername()).isEqualTo("john.doe");
  }
}

@SpringBootTest  
@TestPropertySource(properties = "logging.level.com.example=INFO")
class PaymentServiceIT {

  @Autowired
  private PaymentService paymentService;

  @Test
  void testProcessPayment() {
    PaymentResult result = paymentService.processPayment(new BigDecimal("100.00"));
    assertThat(result.isSuccessful()).isTrue();
  }
}

Эти два класса тестов выглядят схожими, но из-за различий в параметрах уровня логирования они требуют разных Spring-контекстов.

Spring вынужден создавать отдельные контексты для каждого из них, упуская возможность повторного использования кэшированного контекста, что значительно увеличивает время выполнения тестов.

В крупных приложениях задача усложняется: могут существовать десятки классов тестов с едва заметными различиями в конфигурации.

Без надлежащих инструментов выявить такие неэффективности практически невозможно, и разработчики, сами того не подозревая, создают узкие места в производительности с каждым новым добавленным тестом.

Spring Test Profiler: выявление узких мест в производительности

Spring Test Profiler устраняет проблему отсутствия прозрачности в производительности тестов Spring Boot, предоставляя подробную информацию о поведении механизма кэширования контекста и шаблонах выполнения тестов.

Этот инструмент с открытым исходным кодом анализирует выполнение тестов и формирует подробные отчёты, позволяющие выявить возможности для оптимизации.

Профилировщик интегрируется с жизненным циклом выполнения тестов в Spring, фиксируя подробные метрики, связанные с созданием и повторным использованием контекстов, а также с общей производительностью тестов.

Он отслеживает, какие тесты используют общие контексты, какие вызывают затратный процесс создания нового контекста и где можно объединить конфигурации для повышения эффективности кэширования.

Установка и настройка

Интеграция Spring Test Profiler в проект требует минимальной настройки.

Добавим зависимость в файл сборки Maven:

<dependency>
  <groupId>digital.pragmatech.testing</groupId>
  <artifactId>spring-test-profiler</artifactId>
  <!-- check latest version -->
  <version>0.0.11</version> 
  <scope>test</scope>
</dependency>

Далее активируем профилировщик, добавив файл META-INF/spring.factories в каталог тестовых ресурсов:

org.springframework.test.context.TestExecutionListener=\
digital.pragmatech.testing.SpringTestProfilerListener
org.springframework.context.ApplicationContextInitializer=\
digital.pragmatech.testing.diagnostic.ContextDiagnosticApplicationInitializer

Эта конфигурация регистрирует слушатели (listeners) профилировщика в тестовом фреймворке Spring, позволяя ему отслеживать выполнение тестов без необходимости вносить изменения в существующие классы тестов.

Остаётся просто запустить тесты, например, с помощью команды ./mvnw verify.

Профилировщик совместим с Maven и Gradle, Java 17+ и Spring Framework 6.x (Spring Boot 3.x), что делает его подходящим для современных приложений на Spring Boot.

После настройки он автоматически начинает собирать данные о производительности во время выполнения тестов.

Анализ данных о производительности тестов

После запуска тестового набора с включённым профилировщиком мы получаем подробные отчёты в каталоге target/spring-test-profiler (или build/spring-test-profiler для проектов на Gradle), которые раскрывают скрытые характеристики производительности наших тестов.

Профилировщик формирует как вывод в консоль, так и полноформатные HTML-отчёты с практическими рекомендациями.

Отчёты содержат несколько ключевых показателей производительности:

  • Анализ перезагрузки контекста показывает, какие тесты вызывают затратное пересоздание Spring-контекста, и объясняет, почему не удалось переиспользовать существующий контекст. Эта информация помогает понять конкретные различия в конфигурации, мешающие совместному использованию контекста.

  • Рекомендации по оптимизации кэша предлагают конкретные шаги для улучшения повторного использования контекстов в тестовом наборе. Как правило, рекомендации сосредоточены на унификации конфигураций тестов и устранении ненужных различий.

  • Узкие места производительности выделяют самые медленные тесты и количественно оценивают влияние загрузки контекста. Эти данные помогают расставить приоритеты в оптимизации там, где это даст наибольший эффект.

Профилировщик также отслеживает шаблоны использования ресурсов, отображая показатели потребления памяти и времени выполнения, что позволяет понять реальную стоимость выбранных конфигураций тестов.

Кроме того, можно сравнить два тестовых контекста бок о бок, чтобы выявить различия между ними:

В планах также — добавить статический анализ конфигурации тестов, чтобы выявлять потенциальные препятствия для корректной работы механизма кэширования.

В частности, будут отображаться предупреждения при использовании аннотации @DirtiesContext в тестах:

Итоги по Spring Test Profiler

Spring Test Profiler превращает оптимизацию производительности тестов из догадок в процесс, основанный на данных. Предоставляя детальную информацию о работе механизма кэширования Spring TestContext, он позволяет выявить и устранить несогласованности в конфигурации, приводящие к затратным перезагрузкам контекста.

Интегрируя Spring Test Profiler в процесс разработки, мы можем сохранить быстрые циклы обратной связи, поощрять полноценное тестирование и, в конечном счёте, поставлять более качественное программное обеспечение. Время, потраченное на настройку и оптимизацию, окупается за счёт роста производительности разработчиков и надёжности процессов непрерывной интеграции.

Быстрые тесты — залог быстрой разработки.

Обладая нужными инструментами и пониманием механизма кэширования TestContext в Spring, мы можем сделать так, чтобы наборы тестов были не помехой, а полноценным активом в продуктивной разработке на Spring Boot.


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

Комментарии (1)


  1. Throwable
    26.08.2025 17:20

    Что хуже, любой прописанный mock тоже создаёт новый контекст. Подобный дизайн приводит к тому, что многие делают общий класс для всех интеграционных тестов, который содержит всевозможные моки, и потом наследуются от него.