Повторяемые и динамические тесты, порядок тестов
Обычно один метод с @Test — это один тест. В JUnit есть ещё два вида. Повторяемый тест запускает один и тот же метод несколько раз — это удобно, когда результат может зависеть от номера запуска или когда хочется убедиться, что тест стабилен. Динамический тест создаёт ваш код прямо во время прогона: фабричный метод возвращает набор тестов, и JUnit запускает каждый из них. В конце урока — как задать порядок тестов и почему он почти никогда не нужен.
@RepeatedTest
Создайте файл src/test/java/com/example/blog/RepositoryRepeatedTest.java:
// src/test/java/com/example/blog/RepositoryRepeatedTest.java
package com.example.blog;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RepositoryRepeatedTest {
private final InMemoryPostRepository repository = new InMemoryPostRepository();
@RepeatedTest(value = 3, name = "{displayName} {currentRepetition}/{totalRepetitions}")
void idsGrowOneByOne(RepetitionInfo info) {
int count = info.getCurrentRepetition();
Post last = null;
for (int i = 0; i < count; i++) {
last = repository.save(new Post(null, "Пост " + i, "anna", false));
}
assertEquals(count, last.id());
assertEquals(count, repository.findAll().size());
}
}@RepeatedTest(3) запускает метод три раза. Каждый повтор — отдельный тест с новым экземпляром класса, поэтому репозиторий каждый раз пустой. Параметр RepetitionInfo сообщает номер текущего повтора (getCurrentRepetition(), с 1) и их общее число (getTotalRepetitions()). Тест сохраняет один, два, а затем три поста и проверяет, что id идут 1, 2, 3. Благодаря шаблону name в IDE запуски называются idsGrowOneByOne(RepetitionInfo) 1/3, 2/3, 3/3.
[INFO] Running com.example.blog.RepositoryRepeatedTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.030 s -- in com.example.blog.RepositoryRepeatedTestRepetitionInfo работает только в повторяемом тесте. В обычный @Test JUnit передать его не может: ParameterResolutionException: No ParameterResolver registered for parameter [org.junit.jupiter.api.RepetitionInfo arg0].
Динамические тесты: @TestFactory
Параметризованный тест берёт данные из аннотаций или метода. Динамический идёт дальше: сами тесты вместе с их названиями строит код — например, из списка примеров или из файлов в папке.
// src/test/java/com/example/blog/SlugsDynamicTest.java
package com.example.blog;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
class SlugsDynamicTest {
record Example(String title, String slug) {
}
private final List<Example> examples = List.of(
new Example("Hello", "hello"),
new Example("Hello, World!", "hello-world"),
new Example("JUnit & Mockito", "junit-mockito")
);
@TestFactory
Stream<DynamicTest> slugs() {
return examples.stream()
.map(example -> dynamicTest(
"slug для '" + example.title() + "'",
() -> assertEquals(example.slug(), Slugs.of(example.title()))
));
}
}Фабричный метод помечен @TestFactory и возвращает Stream, Collection или другой набор DynamicTest. dynamicTest(название, код) создаёт один тест: отображаемое имя и лямбду с проверками. Maven насчитывает три теста:
[INFO] Running com.example.blog.SlugsDynamicTest
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.025 s -- in com.example.blog.SlugsDynamicTestЕсли один пример падает — скажем, ожидается slug hello-world!, — Surefire показывает его номер, а IDE — его имя slug для 'Hello, World!':
[ERROR] com.example.blog.SlugsDynamicTest.slugs()[2] -- Time elapsed: 0.007 s <<< FAILURE!
org.opentest4j.AssertionFailedError: expected: <hello-world!> but was: <hello-world>Учтите, что @BeforeEach и @AfterEach выполняются один раз вокруг всего фабричного метода, а не вокруг каждого динамического теста. Когда данные помещаются в @CsvSource или @MethodSource, параметризованный тест проще; @TestFactory берите, когда сам набор тестов вычисляется.
Порядок тестов
По умолчанию JUnit запускает методы класса в порядке, который постоянен, но намеренно неочевиден. Если нужен определённый порядок, задайте его на классе:
// src/test/java/com/example/blog/PostStepsTest.java
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class PostStepsTest {
private final PostService service =
new PostService(new InMemoryPostRepository(), (to, message) -> { });
@Test
@Order(1)
void create() {
assertEquals("Привет", service.create(" Привет ", "anna").title());
}
@Test
@Order(2)
void publish() {
Post post = service.create("Привет", "anna");
assertTrue(service.publish(post.id()).published());
}
}Теперь тесты идут по номеру из @Order: сначала create, потом publish. Другие варианты: MethodOrderer.MethodName (по алфавиту), DisplayName, Random.
Заметьте: publish() создаёт свой пост, а не берёт пост из create(). Это главное правило: порядок может сделать отчёт красивее, но тест не должен от него зависеть. Каждый тест готовит свои данные сам — иначе запуск одного теста отдельно или параллельно его сломает (см. урок «Каким должен быть хороший тест»). По-настоящему порядок полезен разве что в интеграционных тестах с медленными шагами, да и там его лучше избегать.
Частая ошибка
Фабричный метод ничего не возвращает. Код внутри создаёт тест, но JUnit его так и не получает:
@TestFactory
void slugs() {
dynamicTest("slug для 'Hello'", () -> assertEquals("hello", Slugs.of("Hello")));
}Сборка зелёная, но ничего не проверено:
WARNING: TestEngine with ID 'junit-jupiter' encountered a non-critical issue during test discovery:
(1) [WARNING] @TestFactory method 'void com.example.blog.VoidFactoryTest.slugs()' must return a single org.junit.jupiter.api.DynamicNode or a Stream, Collection, Iterable, Iterator, Iterator provider, or array of org.junit.jupiter.api.DynamicNode. It will not be executed.
...
[INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESSСледите за Tests run: 0 и предупреждениями при поиске тестов. Метод с @TestFactory должен вернуть тесты: Stream<DynamicTest>, List<DynamicTest> или один DynamicTest.