Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Повторяемые и динамические тесты, порядок тестов

Обычно один метод с @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.RepositoryRepeatedTest

RepetitionInfo работает только в повторяемом тесте. В обычный @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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.