Аннотации JUnit в Java
Основные аннотации JUnit 5 — @Test, @BeforeEach, @AfterEach, @BeforeAll, @AfterAll, @Disabled.
JUnit 5 — это стандартный фреймворк тестирования для Java, и почти всё взаимодействие с ним происходит через аннотации. Вы не пишете метод main и не вызываете методы вручную; вы украшаете обычные методы аннотациями вроде @Test, @BeforeEach и @AfterAll, а движок JUnit обнаруживает их с помощью рефлексии и запускает в нужном порядке. В этой главе рассматриваются основные аннотации жизненного цикла — что означает каждая из них, когда она срабатывает и как они сочетаются, чтобы обеспечить каждому тесту чистое изолированное окружение.
Если вы только начинаете знакомство с фреймворком, начните с введения в JUnit; об утверждениях, используемых в тестах, читайте в разделе утверждения JUnit. Аннотации как общая возможность Java рассматриваются в главе аннотации Java.
Аннотации находятся в org.junit.jupiter.api
API JUnit 5 — это модуль Jupiter. Аннотации, которые вы используете ежедневно, поступают из одного пакета:
| Аннотация | Применяется к | Запускается |
|---|---|---|
@Test | метод | один раз для каждого тестового метода |
@BeforeEach | метод | перед каждым @Test |
@AfterEach | метод | после каждого @Test |
@BeforeAll | static-метод | один раз, до любого теста в классе |
@AfterAll | static-метод | один раз, после всех тестов в классе |
@Disabled | метод или класс | никогда (пропускается и фиксируется) |
@DisplayName | метод или класс | задаёт понятное имя в отчётах |
Метод, помеченный @Test, не требует модификатора public в JUnit 5 (package-private допустим) и должен возвращать void.
@Test: единица работы
Тестовый метод проверяет что-либо с помощью статических помощников из org.junit.jupiter.api.Assertions. Если утверждение не выполняется, оно выбрасывает исключение, и движок фиксирует этот тест как провалившийся, не останавливая остальные.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void addsTwoNumbers() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3));
}
@Test
void throwsOnDivideByZero() {
Calculator calc = new Calculator();
assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
}
}Каждый @Test запускается на новом экземпляре тестового класса — JUnit по умолчанию создаёт новый объект для каждого теста, поэтому поля, установленные в одном тесте, не могут просочиться в другой.
@BeforeEach и @AfterEach: фикстуры для каждого теста
Настройка, необходимая каждому тесту, помещается в метод @BeforeEach; завершение — в @AfterEach. Они обрамляют каждый @Test, давая каждому тесту одинаковую отправную точку.
import org.junit.jupiter.api.*;
class OrderServiceTest {
private OrderService service;
@BeforeEach
void setUp() {
service = new OrderService(new InMemoryRepo()); // fresh state per test
}
@AfterEach
void tearDown() {
service.close(); // runs even if the test threw
}
@Test
void placesOrder() {
assertTrue(service.place("SKU-1", 2));
}
}@AfterEach выполняется даже при сбое теста, что делает его подходящим местом для освобождения ресурсов, открытых в @BeforeEach.
@BeforeAll и @AfterAll: один раз на класс
Когда инициализация дорогостоящая и допускает совместное использование — контейнер базы данных, запущенный встроенный сервер — используйте @BeforeAll, чтобы выполнить её один раз, и @AfterAll, чтобы завершить один раз. Поскольку они выполняются до создания каких-либо экземпляров, они должны быть static.
import org.junit.jupiter.api.*;
class RepositoryTest {
static Database db;
@BeforeAll
static void startDatabase() {
db = Database.start(); // runs once, before everything
}
@AfterAll
static void stopDatabase() {
db.stop(); // runs once, after everything
}
@Test
void savesRow() {
assertEquals(1, db.insert("hello"));
}
}Полный порядок жизненного цикла для класса с двумя тестами: @BeforeAll → (@BeforeEach → @Test → @AfterEach) → (@BeforeEach → @Test → @AfterEach) → @AfterAll. Глава жизненный цикл JUnit подробно рассматривает этот порядок, включая взаимодействие с созданием экземпляров.
@Disabled: пропуск без удаления
@Disabled отключает тест (или целый класс). Движок сообщает о нём как о пропущенном, а не пройденном или провалившемся, поэтому он остаётся видимым. Всегда указывайте причину.
@Test
@Disabled("flaky until the rate-limiter fix lands — see JIRA-1234")
void callsExternalApi() {
// not executed
}Рабочий пример: мини-движок тестирования
В этом запускаемом окружении нет jar-файла JUnit, поэтому программа ниже создаёт собственный маленький движок той же формы, что и JUnit. Она объявляет маркерные аннотации (@BeforeAll, @BeforeEach, @Test, @AfterEach, @AfterAll, @Disabled), определяет небольшой аннотированный тестовый класс, а затем использует рефлексию — именно то, что делает движок JUnit изнутри — для обнаружения и запуска методов в порядке жизненного цикла и вывода итогового отчёта пройдено/провалено/пропущено.
Что следует извлечь из запуска:
@BeforeAllнапечатал ровно один раз в самом начале, а@AfterAll— ровно один раз в самом конце: инициализация и завершение на уровне класса обрамляют весь запуск, именно поэтому JUnit требует, чтобы они былиstatic.- Каждому выполненному
@Testпредшествует строка@BeforeEach, а после него следует строка@AfterEach, так что каждый тест запускается на свежеподготовленной фикстуре и сам убирает за собой — перемежающееся обрамление, которое сохраняет независимость тестов. - Метод
flakyбыл помечен@Disabled, поэтому напечатал(skipped via @Disabled), и его тело никогда не выполнялось; провальное утверждение внутри него не было достигнуто — в этом и смысл отключения вместо удаления. - Цикл обнаружения обрабатывает только методы, для которых
isAnnotationPresent(Test.class)возвращаетtrue— аннотации являются лишь метаданными, и именно движок, читающий их с помощью рефлексии, превращает их в поведение, точно так же, как это делает настоящий JUnit. - Последняя строка сообщает
2 passed, 0 failed, 1 skipped: два реальных теста прошли, ни один не провалился, а отключённый засчитан как пропущенный, а не проигнорирован молча — такой же учёт пройдено/провалено/пропущено, как в отчёте JUnit.