Guide to Dynamic Tests in Junit 5 – Junit 5中的动态测试指南

最后修改: 2017年 5月 31日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Dynamic testing is a new programming model introduced in JUnit 5. In this article, we’ll have a look at what exactly dynamic tests are and how to create them.

动态测试是JUnit 5中引入的一个新的编程模型。在这篇文章中,我们将看看到底什么是动态测试以及如何创建它们。

If you’re completely new to JUnit 5, you might want to check the preview of JUnit 5 and our primary guide.

如果你完全不了解JUnit 5,你可能想看看JUnit 5的预览我们的初级指南

2. What Is a DynamicTest?

2.什么是DynamicTest

The standard tests annotated with @Test annotation are static tests which are fully specified at the compile time. A DynamicTest is a test generated during runtime. These tests are generated by a factory method annotated with the @TestFactory annotation.

@Test注解的标准测试是静态测试,在编译时完全指定。一个DynamicTest是在运行时生成的测试。这些测试是由一个用@TestFactory注解的工厂方法生成的。

A @TestFactory method must return a Stream, Collection, Iterable, or Iterator of DynamicTest instances. Returning anything else will result in a JUnitException since the invalid return types cannot be detected at compile time. Apart from this, a @TestFactory method cannot be static or private.

@TestFactory方法必须返回StreamCollectionIterableIteratorDynamicTest实例。返回其他任何东西都会导致JUnitException,因为无效的返回类型在编译时无法被检测到。除此之外,@TestFactory方法不能是静态cprivate

The DynamicTests are executed differently than the standard @Tests and do not support lifecycle callbacks. Meaning, the @BeforeEach and the @AfterEach methods will not be called for the DynamicTests.

DynamicTests的执行方式与标准的@Tests不同,不支持生命周期回调。这意味着,@BeforeEach@AfterEach方法将不会被DynamicTests调用。

3. Creating DynamicTests

3.创建DynamicTests

First, let’s have a look at different ways of creating DynamicTests.

首先,让我们看一下创建DynamicTests的不同方法。

The examples here are not dynamic in nature, but they’ll provide a good starting point for creating truly dynamic ones.

这里的例子在本质上不是动态的,但它们将为创建真正的动态例子提供一个良好的起点。

We’re going to create a Collection of DynamicTest:

我们将创建一个CollectionDynamicTest

@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))));
}

The @TestFactory method tells JUnit that this is a factory for creating dynamic tests. As we can see, we’re only returning a Collection of DynamicTest. Each of the DynamicTest consists of two parts, the name of the test or the display name, and an Executable.

@TestFactory方法告诉JUnit,这是一个创建动态测试的工厂。我们可以看到,我们只返回一个CollectionDynamicTest每个DynamicTest由两部分组成,测试的名称或显示名称,以及一个Executable

The output will contain the display name that we passed to the dynamic tests:

输出将包含我们传递给动态测试的显示名称。

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

The same test can be modified to return an Iterable, Iterator, or a Stream:

同样的测试可以被修改为返回一个IterableIterator,或者一个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)));
}

Please note that if the @TestFactory returns a Stream, then it will be automatically closed once all the tests are executed.

请注意,如果@TestFactory返回一个Stream,那么一旦所有测试被执行,它将被自动关闭。

The output will be pretty much the same as the first example. It will contain the display name that we pass to the dynamic test.

输出将与第一个例子基本相同。它将包含我们传递给动态测试的显示名称。

4. Creating a Stream of DynamicTests

4.创建一个StreamDynamicTests

For the demonstration purposes, consider a DomainNameResolver which returns an IP address when we pass the domain name as input.

为了演示的目的,考虑一个DomainNameResolver,当我们把域名作为输入时,它返回一个IP地址。

For the sake of simplicity, let’s have a look at the high-level skeleton of our factory method:

为了简单起见,让我们看看我们的工厂方法的高级骨架。

@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...*/
}

There isn’t much code related to DynamicTest here apart from the @TestFactory annotation, which we’re already familiar with.

除了我们已经熟悉的@TestFactory注解外,这里没有多少与DynamicTest相关的代码。

The two ArrayLists will be used as input to DomainNameResolver and expected output respectively.

这两个ArrayLists将分别作为DomainNameResolver的输入和预期输出。

Let’s now have a look at the input generator:

现在让我们看一下输入发生器。

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

The input generator is nothing but an Iterator of String. It uses our inputList and returns the domain name one by one.

输入发生器只不过是一个StringIterator。它使用我们的inputList并逐一返回域名。

The display name generator is fairly simple:

显示名称生成器是相当简单的。

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

The task of a display name generator is just to provide a display name for the test case that will be used in JUnit reports or the JUnit tab of our IDE.

显示名称生成器的任务只是为测试用例提供一个显示名称,它将在JUnit报告或我们IDE的JUnit标签中使用。

Here we are just utilizing the domain name to generate unique names for each test. It’s not required to create unique names, but it will help in case of any failure. Having this, we’ll be able to tell the domain name for which the test case failed.

在这里,我们只是利用域名来为每个测试生成独特的名字。创建唯一的名字不是必须的,但它会在任何失败的情况下有所帮助。有了这个,我们就能知道测试案例失败的域名。

Now let’s have a look at the central part of our test – the test execution code:

现在让我们来看看我们测试的核心部分–测试执行代码:

DomainNameResolver resolver = new DomainNameResolver();
ThrowingConsumer<String> testExecutor = (input) -> {
    int id = inputList.indexOf(input);
 
    assertEquals(outputList.get(id), resolver.resolveDomain(input));
};

We have used the ThrowingConsumer, which is a @FunctionalInterface for writing the test case. For each input generated by the data generator, we’re fetching the expected output from the outputList and the actual output from an instance of DomainNameResolver.

我们使用了ThrowingConsumer,它是一个@FunctionalInterface,用于编写测试用例。对于数据生成器产生的每个输入,我们从outputList中获取预期输出,从DomainNameResolver的实例中获取实际输出。

Now the last part is simply to assemble all the pieces and return as a Stream of DynamicTest:

现在,最后一部分是简单地将所有的部分组合起来,并作为StreamDynamicTest返回。

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

That’s it. Running the test will display the report containing the names defined by our display name generator:

这就是了。运行测试将显示包含由我们的显示名称生成器定义的名称的报告。

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

5. Improving the DynamicTest Using Java 8 Features

5.使用Java 8功能改进DynamicTest

The test factory written in the previous section can be drastically improved by using the features of Java 8. The resultant code will be much cleaner and can be written in a lesser number of lines:

通过使用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));
    }));       
}

The above code has the same effect as the one we saw in the previous section. The inputList.stream().map() provides the stream of inputs (input generator). The first argument to dynamicTest() is our display name generator (“Resolving: ” + dom) while the second argument, a lambda, is our test executor.

上面的代码与我们在上一节看到的代码有相同的效果。inputList.stream().map()提供了输入的流(输入生成器)。dynamicTest()的第一个参数是我们的显示名称生成器(”Resolving: ” + dom),而第二个参数,一个lambda,是我们的测试执行器。

The output will be the same as the one from the previous section.

其输出结果将与上一节的输出结果相同。

6. Additional Example

6.额外的例子

In this example, we’re further exploring the power of the dynamic tests to filter the inputs based on the test cases:

在这个例子中,我们要进一步探索动态测试的力量,根据测试案例过滤输入。

@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);
}

The save(Long) method needs only the employeeId. Hence, it utilizes all the Employee instances. The save(Long, String) method needs firstName apart from the employeeId. Hence, it filters out the Employee instances without firstName.

save(Long)方法只需要employeeId。因此,它利用了所有的Employee实例。save(Long, String)方法除了需要firstName,还需要employeeId。因此,它过滤掉了没有firstName的Employee实例。

Finally, we combine both the streams and return all the tests as a single Stream.

最后,我们将两个流合并,并将所有测试作为一个单一的Stream返回。

Now, let’s have a look at the output:

现在,让我们看一下输出。

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

7.结论

The parameterized tests can replace many of the examples in this article. However, the dynamic tests differ from the parameterized tests as they do not support full test lifecycle, while parametrized tests do.

参数化测试可以取代本文中的许多例子。然而,动态测试与参数化测试不同,它们不支持完整的测试生命周期,而参数化测试支持。

Moreover, dynamic tests provide more flexibility regarding how the input is generated and how the tests are executed.

此外,动态测试在如何生成输入和如何执行测试方面提供了更多的灵活性。

JUnit 5 prefers extensions over features principle. As a result, the main aim of dynamic tests is to provide an extension point for third party frameworks or extensions.

JUnit 5更倾向于扩展而非功能原则。因此,动态测试的主要目的是为第三方框架或扩展提供一个扩展点。

You can read more about other features of JUnit 5 in our article on repeated tests in JUnit 5.

您可以在我们的关于JUnit 5中重复测试的文章中阅读更多关于JUnit 5的其他功能。

Don’t forget to check out the full source code of this article on GitHub.

不要忘记在GitHub上查看本文章的完整源代码