Перейти к основному содержимому

Руководство по динамическим тестам в Junit 5

· 6 мин. чтения

1. Обзор

Динамическое тестирование — это новая модель программирования, представленная в JUnit 5. В этой статье мы рассмотрим, что такое динамические тесты и как их создавать.

Если вы новичок в JUnit 5, вы можете ознакомиться с предварительной версией JUnit 5 и нашим основным руководством .

2. Что такое динамический тест ?

Стандартные тесты, помеченные аннотацией @Test , являются статическими тестами, которые полностью определяются во время компиляции. DynamicTest — это тест, созданный во время выполнения . Эти тесты генерируются фабричным методом, снабженным аннотацией @TestFactory .

Метод @TestFactory должен возвращать Stream , Collection , Iterable или Iterator экземпляров DynamicTest . Возврат чего-либо еще приведет к JUnitException , поскольку недопустимые возвращаемые типы не могут быть обнаружены во время компиляции. Кроме того, метод @TestFactory не может быть статическим или закрытым .

DynamicTest выполняются иначе, чем стандартные @Test , и не поддерживают обратные вызовы жизненного цикла. Это означает, что методы @BeforeEach и @AfterEach не будут вызываться для DynamicTest s .

3. Создание динамических тестов

Во-первых, давайте рассмотрим различные способы создания DynamicTest s.

Приведенные здесь примеры не являются динамическими по своей природе, но они послужат хорошей отправной точкой для создания действительно динамичных примеров.

Мы собираемся создать коллекцию DynamicTest :

@TestFactory
Collection<DynamicTest> dynamicTestsWithCollection() {
return Arrays.asList(
DynamicTest.dynamicTest("Add test",
() -> assertEquals(2, Math.addExact(1, 1))),
DynamicTest.dynamicTest("Multiply Test",
() -> assertEquals(4, Math.multiplyExact(2, 2))));
}

Метод @TestFactory сообщает JUnit, что это фабрика для создания динамических тестов. Как мы видим, мы возвращаем только коллекцию DynamicTest . Каждый из DynamicTest состоит из двух частей: имени теста или отображаемого имени и исполняемого файла . ** ``**

Вывод будет содержать отображаемое имя, которое мы передали в динамические тесты:

Add test(dynamicTestsWithCollection())
Multiply Test(dynamicTestsWithCollection())

Тот же тест можно изменить, чтобы он возвращал Iterable , Iterator или Stream :

@TestFactory
Iterable<DynamicTest> dynamicTestsWithIterable() {
return Arrays.asList(
DynamicTest.dynamicTest("Add test",
() -> assertEquals(2, Math.addExact(1, 1))),
DynamicTest.dynamicTest("Multiply Test",
() -> assertEquals(4, Math.multiplyExact(2, 2))));
}

@TestFactory
Iterator<DynamicTest> dynamicTestsWithIterator() {
return Arrays.asList(
DynamicTest.dynamicTest("Add test",
() -> assertEquals(2, Math.addExact(1, 1))),
DynamicTest.dynamicTest("Multiply Test",
() -> assertEquals(4, Math.multiplyExact(2, 2))))
.iterator();
}

@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> DynamicTest.dynamicTest("test" + n,
() -> assertTrue(n % 2 == 0)));
}

Обратите внимание, что если @TestFactory возвращает Stream , то он будет автоматически закрыт после выполнения всех тестов.

Вывод будет почти таким же, как и в первом примере. Он будет содержать отображаемое имя, которое мы передаем динамическому тесту.

4. Создание потока динамических тестов

В демонстрационных целях рассмотрим DomainNameResolver , который возвращает IP-адрес, когда мы передаем доменное имя в качестве входных данных.

Для простоты давайте взглянем на высокоуровневый скелет нашего фабричного метода:

@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {

// sample input and output
List<String> inputList = Arrays.asList(
"www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
List<String> outputList = Arrays.asList(
"154.174.10.56", "211.152.104.132", "178.144.120.156");

// input generator that generates inputs using inputList
/*...code here...*/

// a display name generator that creates a
// different name based on the input
/*...code here...*/

// the test executor, which actually has the
// logic to execute the test case
/*...code here...*/

// combine everything and return a Stream of DynamicTest
/*...code here...*/
}

Здесь не так много кода, связанного с DynamicTest , кроме аннотации @TestFactory , с которой мы уже знакомы.

Два ArrayList будут использоваться в качестве входных данных для DomainNameResolver и ожидаемых выходных данных соответственно.

Давайте теперь посмотрим на генератор ввода:

Iterator<String> inputGenerator = inputList.iterator();

Генератор ввода — это не что иное, как Iterator of String . Он использует наш inputList и возвращает доменное имя одно за другим.

Генератор отображаемого имени довольно прост:

Function<String, String> displayNameGenerator 
= (input) -> "Resolving: " + input;

Задача генератора отображаемых имен — просто предоставить отображаемое имя для тестового примера, которое будет использоваться в отчетах JUnit или на вкладке JUnit нашей IDE.

Здесь мы просто используем доменное имя для создания уникальных имен для каждого теста. Не обязательно создавать уникальные имена, но это поможет в случае сбоя. Имея это, мы сможем указать доменное имя, для которого тестовый пример не удался.

Теперь давайте посмотрим на центральную часть нашего теста — код выполнения теста:

DomainNameResolver resolver = new DomainNameResolver();
ThrowingConsumer<String> testExecutor = (input) -> {
int id = inputList.indexOf(input);

assertEquals(outputList.get(id), resolver.resolveDomain(input));
};

Мы использовали ThrowingConsumer , который является @FunctionalInterface для написания тестового примера. Для каждого ввода, сгенерированного генератором данных, мы получаем ожидаемый вывод из outputList и фактический вывод из экземпляра DomainNameResolver .

Теперь последняя часть — просто собрать все части и вернуть как поток DynamicTest :

return DynamicTest.stream(
inputGenerator, displayNameGenerator, testExecutor);

Вот и все. Запуск теста отобразит отчет, содержащий имена, определенные нашим генератором отображаемых имен:

Resolving: www.somedomain.com(dynamicTestsFromStream())
Resolving: www.anotherdomain.com(dynamicTestsFromStream())
Resolving: www.yetanotherdomain.com(dynamicTestsFromStream())

5. Улучшение DynamicTest с использованием функций Java 8

Фабрику тестов, написанную в предыдущем разделе, можно значительно улучшить, используя возможности Java 8. Результирующий код будет намного чище и может быть записан в меньшем количестве строк:

@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamInJava8() {

DomainNameResolver resolver = new DomainNameResolver();

List<String> domainNames = Arrays.asList(
"www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
List<String> outputList = Arrays.asList(
"154.174.10.56", "211.152.104.132", "178.144.120.156");

return inputList.stream()
.map(dom -> DynamicTest.dynamicTest("Resolving: " + dom,
() -> {int id = inputList.indexOf(dom);

assertEquals(outputList.get(id), resolver.resolveDomain(dom));
}));
}

Приведенный выше код имеет тот же эффект, что и тот, который мы видели в предыдущем разделе. inputList.stream ().map() предоставляет поток входных данных (генератор входных данных). Первый аргумент для dynamicTest() — это наш генератор отображаемого имени («Разрешение:» + dom ), а второй аргумент, лямбда , — наш исполнитель теста.

Вывод будет таким же, как и в предыдущем разделе.

6. Дополнительный пример

В этом примере мы дополнительно изучаем возможности динамических тестов для фильтрации входных данных на основе тестовых случаев:

@TestFactory
Stream<DynamicTest> dynamicTestsForEmployeeWorkflows() {
List<Employee> inputList = Arrays.asList(
new Employee(1, "Fred"), new Employee(2), new Employee(3, "John"));

EmployeeDao dao = new EmployeeDao();
Stream<DynamicTest> saveEmployeeStream = inputList.stream()
.map(emp -> DynamicTest.dynamicTest(
"saveEmployee: " + emp.toString(),
() -> {
Employee returned = dao.save(emp.getId());
assertEquals(returned.getId(), emp.getId());
}
));

Stream<DynamicTest> saveEmployeeWithFirstNameStream
= inputList.stream()
.filter(emp -> !emp.getFirstName().isEmpty())
.map(emp -> DynamicTest.dynamicTest(
"saveEmployeeWithName" + emp.toString(),
() -> {
Employee returned = dao.save(emp.getId(), emp.getFirstName());
assertEquals(returned.getId(), emp.getId());
assertEquals(returned.getFirstName(), emp.getFirstName());
}));

return Stream.concat(saveEmployeeStream,
saveEmployeeWithFirstNameStream);
}

Методу save(Long) нужен только employeeId . Следовательно, он использует все экземпляры Employee . Метод save(Long, String) нуждается в firstName отдельно от employeeId . Следовательно, он отфильтровывает экземпляры Employee без firstName.

Наконец, мы объединяем оба потока и возвращаем все тесты как один поток .

Теперь давайте посмотрим на вывод:

saveEmployee: Employee 
[id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows())
saveEmployee: Employee
[id=2, firstName=](dynamicTestsForEmployeeWorkflows())
saveEmployee: Employee
[id=3, firstName=John](dynamicTestsForEmployeeWorkflows())
saveEmployeeWithNameEmployee
[id=1, firstName=Fred](dynamicTestsForEmployeeWorkflows())
saveEmployeeWithNameEmployee
[id=3, firstName=John](dynamicTestsForEmployeeWorkflows())

7. Заключение

Параметризованные тесты могут заменить многие примеры в этой статье. Однако динамические тесты отличаются от параметризованных тестов тем, что они не поддерживают полный жизненный цикл тестирования, в отличие от параметризованных тестов.

Кроме того, динамические тесты обеспечивают большую гибкость в отношении того, как генерируются входные данные и как выполняются тесты.

JUnit 5 предпочитает принцип расширений функциям . В результате основная цель динамических тестов — предоставить точку расширения для сторонних фреймворков или расширений.

Подробнее о других возможностях JUnit 5 вы можете прочитать в нашей статье о повторных тестах в JUnit 5 .

Не забудьте проверить полный исходный код этой статьи на GitHub .