Жизненный цикл тестов JUnit в Java
Жизненный цикл тестового экземпляра и поведение per-method vs. per-class в JUnit 5.
Каждый тест JUnit 5 выполняется внутри чётко определённого жизненного цикла: последовательности хуков инициализации и завершения, которые вызываются вокруг ваших методов @Test в гарантированном порядке. Понимание этого порядка — и правила, согласно которому JUnit создаёт новый экземпляр тестового класса для каждого тестового метода — отличает нестабильные, зависящие от порядка выполнения тест-сьюты от чистых, изолированных. В этой главе рассматриваются пять аннотаций жизненного цикла и два режима управления экземплярами, которые предлагает JUnit.
Пять аннотаций жизненного цикла
JUnit 5 (пакет org.junit.jupiter.api) определяет четыре аннотации обратного вызова, обрамляющие ваши тесты, плюс сам @Test. Подробное описание каждой аннотации см. в разделе аннотации JUnit; если вы только знакомитесь с фреймворком, начните с введения в JUnit.
| Аннотация | Когда запускается | Метод должен быть |
|---|---|---|
@BeforeAll | Один раз, перед любым тестом в классе | static (в жизненном цикле по умолчанию) |
@BeforeEach | Перед каждым методом @Test | экземплярным |
@Test | Сам тест | экземплярным |
@AfterEach | После каждого метода @Test | экземплярным |
@AfterAll | Один раз, после выполнения всех тестов | static (в жизненном цикле по умолчанию) |
Таким образом, для тестового класса с тремя тестами @BeforeAll вызывается один раз, затем @BeforeEach → @Test → @AfterEach три раза, после чего один раз вызывается @AfterAll.
import org.junit.jupiter.api.*;
class CalculatorTest {
@BeforeAll static void initSuite() { System.out.println("once, up front"); }
@BeforeEach void setUp() { System.out.println("before each test"); }
@Test void add() { Assertions.assertEquals(4, 2 + 2); }
@Test void subtract() { Assertions.assertEquals(0, 2 - 2); }
@AfterEach void tearDown() { System.out.println("after each test"); }
@AfterAll static void close() { System.out.println("once, at the end"); }
}Новый экземпляр для каждого тестового метода
Важнейшее правило жизненного цикла: по умолчанию JUnit создаёт совершенно новый экземпляр тестового класса перед каждым тестовым методом. Поля, изменённые в одном тесте, не могут попасть в другой, поскольку следующий тест выполняется на другом объекте. Именно это делает тесты независимыми от порядка выполнения.
class IsolationTest {
private int counter = 0; // re-initialised for every test
@Test void first() { counter++; Assertions.assertEquals(1, counter); }
@Test void second() { counter++; Assertions.assertEquals(1, counter); } // also 1, not 2
}Оба теста видят counter == 1. Если бы JUnit повторно использовал один экземпляр, второй тест наблюдал бы 2 и прошёл или упал бы в зависимости от порядка выполнения — именно эту нестабильность предотвращает данный подход.
PER_METHOD vs. PER_CLASS
Вы можете отказаться от режима per-method с помощью @TestInstance(Lifecycle.PER_CLASS). Тогда JUnit создаёт один экземпляр для всего класса, поля экземпляра сохраняются между тестами, и — в качестве удобства — методы @BeforeAll/@AfterAll могут быть нестатическими.
| Аспект | PER_METHOD (по умолчанию) | PER_CLASS |
|---|---|---|
| Создаваемые экземпляры | по одному на @Test | один на класс |
| Состояние полей экземпляра | сбрасывается на каждый тест | разделяется между тестами |
@BeforeAll/@AfterAll | должны быть static | могут быть методами экземпляра |
| Лучше всего подходит для | максимальной изоляции | дорогостоящей общей инициализации |
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.TestInstance.Lifecycle;
@TestInstance(Lifecycle.PER_CLASS)
class SharedFixtureTest {
@BeforeAll void openConnection() { /* non-static is now legal */ }
@AfterAll void closeConnection() { }
}Используйте PER_CLASS только тогда, когда инициализация действительно затратна и её можно безопасно использовать совместно. Режим по умолчанию обеспечивает изоляцию бесплатно.
Утверждения — способ сообщить о сбое теста
Жизненный цикл существует для выполнения утверждений. Метод Assertions.assertEquals(expected, actual) выбрасывает AssertionFailedError, когда значения различаются; это прерывает выполнение данного конкретного теста (его @AfterEach всё равно выполняется) и помечает тест как проваленный — остальные тесты продолжают работу. Полный набор методов assert* см. в разделе утверждения JUnit.
import static org.junit.jupiter.api.Assertions.*;
@Test void example() {
assertEquals(42, compute());
assertTrue(isReady());
assertThrows(IllegalArgumentException.class, () -> parse("bad"));
}Практический пример: пошаговая трассировка жизненного цикла
На этой площадке нет JUnit-раннера, поэтому программа ниже моделирует жизненный цикл на чистом коде JDK: она вызывает хуки в порядке JUnit, сравнивает PER_METHOD (новый экземпляр на каждый тест) с PER_CLASS (один общий экземпляр) и завершается небольшим самопроверяющим жгутом в духе assertEquals.
Что можно извлечь из запуска:
- Блок
PER_METHODвыводитinstance#1,instance#2,instance#3для трёх тестов, подтверждая стандартное правило JUnit: новый тестовый экземпляр создаётся для каждого метода@Test, поэтому ни один тест не может видеть изменённое состояние другого теста. - В
PER_METHODкаждая строка[TEST]выводитcounter=1, никогда2или3. Каждый экземпляр получает собственное свежее поле — именно поэтому тесты остаются независимыми от порядка выполнения, что является ключевым преимуществом жизненного цикла по умолчанию. - Блок
PER_CLASSповторно используетinstance#1для всех трёх тестов, и егоcounterрастёт1 → 2 → 3. При одном общем экземпляре состояние поля экземпляра намеренно перетекает между тестами — это полезно для дорогостоящих общих фикстур, но опасно, если забыть об этом. @BeforeAllи@AfterAllпоявляются ровно по одному разу в каждом блоке, обрамляя пары@BeforeEach/@AfterEachуровня теста, которые срабатывают три раза — именно такой порядок вложенности JUnit гарантирует вокруг ваших тестов.- Завершающий жгут выводит
PASS:для всех трёх проверок; неудавшаясяcheckвыбрасываетAssertionErrorс сообщениемFAIL:, зеркально отражая поведениеAssertions.assertEquals, которое прерывает выполнение одного теста сAssertionFailedError, не затрагивая остальные.
Связанные главы
- Введение в JUnit — настройка JUnit 5 и запуск первого теста.
- Аннотации JUnit — все аннотации, формирующие жизненный цикл.
- Утверждения JUnit — как тест сообщает об успехе или провале.
- Параметризованные тесты — запуск одного тела теста с множеством входных данных.