Привет, Хабр!
Сегодня рассмотрим JUnit 5 и разберёмся, чем дышит аннотация @TestInstance(PER_CLASS), — зачем переопределять жизненный цикл тестового инстанса и когда это может помочь.
PER_CLASS — это когда фреймворк создаёт один объект вашего теста на весь класс, а не по объекту на каждый метод. Взамен вы:
-
Можете писать
@BeforeAll/@AfterAllбезstatic. -
Держите дорогие ресурсы (Docker‑контейнер, embedded‑Kafka, что угодно) в поле и не пересоздаёте их по тысяче раз.
-
Рискуете нахвататься shared‑state‑хейтерства, если забыли причесать поля перед следующим тестом.
Рассмотрим подробнее.
Что JUnit думает о жизненном цикле
По дефолту Jupiter создаёт новый экземпляр тестового класса каждый раз перед вызовом метода — модель PER_METHOD. Это историческое наследие борьбы с мутабельностью: нет объекта — нет стейта, нет проблем. Но аннотация
@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyExpensiveIntegrationTest { ... }
меняет правила: объект один, методы бегают поочерёдно внутри него. JUnit официально благословил режим с пояснением, что он полезен, но требует дисциплины — сбросьте состояние сами, если оно вам дорого.
@BeforeAll без static
В PER_CLASS можно писать так:
@BeforeAll void bootKafka() { kafka = Testcontainers.startKafka(); }
Никаких static void, приятно. Под руководство попадают и @Nested классы — там тоже оживает @BeforeAll без статики, что спасает от странных костылей в Java ≤ 15.
Когда это ускоряет сборку
Допустим, в проекте около сотни тестов поднимают embedded ElasticSearch. Каждая инициализация ≈ 800 мс. На CI это превращается в +1 мин к билду. Можно перевести класс‑хозяин на PER_CLASS — и валидацию индексов оставить в @AfterAll.
Пример переезда:
@TestInstance(PER_CLASS) class ElasticSearchIT { private ElasticContainer container; @BeforeAll void startEs() { container = new ElasticContainer("docker.elastic.co/elasticsearch/elasticsearch:8.13.0"); container.start(); } @Test void shouldIndexDocument() { // ... } @AfterAll void stopEs() { container.stop(); } }
|
Режим |
Время, с |
|---|---|
|
PER_METHOD |
78 |
|
PER_CLASS |
19 |
Профит очевиден, но…
Темная сторона shared state
С одним инстансом легко случайно пропылесосить переменной между тестами:
@TestInstance(PER_CLASS) class CounterTest { private int counter = 0; @Test void increments() { counter++; assertEquals(1, counter); } @Test void stillZero() { assertEquals(0, counter); } // }
Феил гарантирован. Правила выживания:
-
Избегайте мутаций. Пусть поля будут
final, а коллекции — immutable. -
Если мутация неизбежна — сбрасывайте её в
@BeforeEach. -
Включили параллельный запуск через
junit.jupiter.execution.parallel.enabled=true? Обеспечьте синхронизацию или откажитесь отPER_CLASSвовсе.
Конфигурация на уровне проекта
Надоело размазывать аннотацию по классам? Положите в src/test/resources файл junit-platform.properties:
junit.jupiter.testinstance.lifecycle.default = per_class
JUnit подхватит настройку глобально. Но будьте осторожны: IDE может запускать тесты со своим рантаймом и проигнорировать property.
Жизненный цикл с DI и Extentions
Spring Boot + JUnit 5
В спринговых тестах @SpringBootTest уже создаёт контекст один раз на класс, так что PER_CLASS как бы логично. Однако Spring тест‑раннер сам решает, когда рестартовать контекст между классами, и дополнительный shared state может смешать карты кэшам и MockBean»ам. Простой критерий: если вы не мутируете бины — смело включайте.
Mockito Extension
@ExtendWith(MockitoExtension.class) @TestInstance(PER_CLASS) class UserServiceTest { ... }
MockitoExtension хранит мок‑прокси внутри каждого тестового экземпляра, так что в PER_CLASS вы получаете одни и те же моки на все методы. Good! Но не забудьте в @BeforeEach делать reset(userRepository) — иначе порядок вызовов смешается.
TestInstanceFactory
С JUnit 5.9 появилась возможность самому создавать экземпляры тестов через TestInstanceFactory. В связке с PER_CLASS можно заинжектить депсы прямиком из Spring‑контейнера или Dagger‑модуля:
public class SpringAwareFactory implements TestInstanceFactory { @Override public Object createTestInstance(TestInstanceFactoryContext ctx, ExtensionContext ex) { ApplicationContext appCtx = SpringExtension.getApplicationContext(ex); return appCtx.getBean(ctx.getTestClass()); } }
Регистрируем через @ExtendWith, экономим конструкторный DI и сохраним state между методами. Но регистрировать две фабрики на класс — ошибка.
Заключение
@TestInstance(PER_CLASS) — это инструмент ускорения тестов и оптимизации ресурсов, а не крутой трюк. Используйте его, когда:
-
инициализация окружения дорога по времени или деньгам;
-
нужен не‑
static@BeforeAll/@AfterAll; -
управление общими ресурсами явно прописано и проверено.
Не включайте, если:
-
тесты запускаются параллельно и состояние нельзя надёжно обнулить;
-
важна независимость между методами (property‑based или fuzz‑тесты);
-
в команде нет чётких договорённостей о работе с shared state.
Перед миграцией:
-
Замерьте профит — до и после.
-
Проверьте мутабельность полей — добавьте очистку в
@BeforeEach, если нужно. -
Прогоните параллель — убедитесь, что нет гонок.
-
Задокументируйте выбор — чтобы решение было прозрачным для всей команды.
В итоге один экземпляр тестового класса способен сэкономить минуты на CI и упростить код, но только при дисциплинированном обращении со стейтом. Действуйте осознанно — и ваши интеграционные тесты будут быстрыми, надёжными и предсказуемыми.
В заключение рекомендую к посещению открытые уроки, которые пройдут в рамках онлайн-курса «Java Developer. Advanced» в OTUS:
-
Юнит тесты для многопоточного кода — 24 июня в 20:00
-
LangChain в Java: Langchain4j, Quarkus, Spring Boot — 17 июля в 20:00
Кроме того, пройдите вступительное тестирование, чтобы оценить свой уровень и узнать, подойдет ли вам программа продвинутого курса по Java.
ссылка на оригинал статьи https://habr.com/ru/articles/920934/
Добавить комментарий