W3docs

Жизненный цикл тестов 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.

java— editable, runs on the server

Что можно извлечь из запуска:

  • Блок 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 5 с жизненным циклом тестового экземпляра по умолчанию: сколько экземпляров тестового класса, содержащего три метода @Test, создаётся при его запуске?
В JUnit 5 с жизненным циклом тестового экземпляра по умолчанию: сколько экземпляров тестового класса, содержащего три метода @Test, создаётся при его запуске?
Was this page helpful?