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

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

1. обзор

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

Если вы новичок в JUnit 5, вы можете проверитьpreview of JUnit 5 иour primary guide.

2. Что такоеDynamicTest?

Стандартные тесты, помеченные аннотацией@Test, являются статическими тестами, которые полностью указываются во время компиляции. A DynamicTest is a test generated during runtime. Эти тесты генерируются фабричным методом с аннотацией@TestFactory.

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

DynamicTests выполняются иначе, чем стандартный@Tests, и не поддерживают обратные вызовы жизненного цикла. Это означает, что@BeforeEach and the @AfterEach methods will not be called for the DynamicTests.

3. СозданиеDynamicTests

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

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

Мы собираемся создатьCollection изDynamicTest:

@TestFactory
Collection 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, что это фабрика для создания динамических тестов. Как мы видим, мы возвращаем толькоCollection изDynamicTest. Each of the DynamicTest consists of two parts, the name of the test or the display name, and an Executable.

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

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

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

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

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

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

4. СозданиеStream изDynamicTests

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

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

@TestFactory
Stream dynamicTestsFromStream() {

    // sample input and output
    List inputList = Arrays.asList(
      "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
    List 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, с которой мы уже знакомы.

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

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

Iterator inputGenerator = inputList.iterator();

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

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

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

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

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

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

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

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

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

Теперь последняя часть - просто собрать все части и вернуть какStream из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 dynamicTestsFromStreamInJava8() {

    DomainNameResolver resolver = new DomainNameResolver();

    List domainNames = Arrays.asList(
      "www.somedomain.com", "www.anotherdomain.com", "www.yetanotherdomain.com");
    List 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() - это наш генератор отображаемых имен («Resolving:» +dom), а второй аргумент,lambda, - это наш тестовый исполнитель.

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

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

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

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

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

    Stream 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.

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

Теперь посмотрим на результат:

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. Заключение

Параметризованные тесты могут заменить многие примеры в этой статье. However, the dynamic tests differ from the parameterized tests as they support full test lifecycle, while parametrized tests don’t.с

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

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

Вы можете узнать больше о других функциях JUnit 5 в нашемarticle on repeated tests in JUnit 5.

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