2. 概述

动态测试是JUnit 5引入的新编程模型。本文将深入探讨动态测试的本质及其创建方法。如果你对JUnit 5完全陌生,建议先阅读JUnit 5预览核心指南

3. 什么是DynamicTest?

标准测试使用@Test注解标记,属于静态测试——在编译时完全确定。而**DynamicTest是运行时生成的测试**,通过@TestFactory注解的工厂方法创建。

@TestFactory方法必须返回DynamicTest实例的StreamCollectionIterableIterator。返回其他类型会抛出JUnitException,因为无效返回类型在编译时无法检测。此外,@TestFactory方法不能是staticprivate

⚠️ 关键区别DynamicTest的执行方式与标准@Test不同,不支持生命周期回调——@BeforeEach@AfterEach不会对动态测试生效。

4. 创建DynamicTest

先看几种创建DynamicTest的基础方式。这些示例虽非真正动态,但能帮我们理解核心概念。

返回Collection的示例

@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包含两部分:

  • 测试显示名称
  • Executable执行逻辑

输出将显示我们定义的名称:

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

其他返回类型示例

相同逻辑可改用IterableIteratorStream

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

注意:返回Stream时,测试执行完成后会自动关闭流。

5. 创建DynamicTest流

以域名解析器DomainNameResolver为例,输入域名返回IP地址。先看工厂方法骨架:

@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {

    // 测试输入和预期输出
    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");

    // 输入生成器(使用inputList)
    /*...代码实现...*/

    // 显示名称生成器(基于输入创建名称)
    /*...代码实现...*/

    // 测试执行器(包含测试逻辑)
    /*...代码实现...*/

    // 组合所有组件返回DynamicTest流
    /*...代码实现...*/
}

输入生成器

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

简单使用inputList的迭代器逐个返回域名。

显示名称生成器

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

为测试生成唯一名称(虽非强制,但失败时便于定位问题)。

测试执行器(核心部分)

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

使用ThrowingConsumer(函数式接口)实现测试逻辑:根据输入域名获取预期输出和实际输出进行断言。

组装返回流

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

运行后输出:

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

6. 用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()的第一个参数是显示名称生成器,第二个lambda是测试执行器。输出与前节完全一致。

7. 进阶示例

此例展示动态测试的强大能力——根据测试用例过滤输入:

@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,过滤掉无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())

8. 总结

参数化测试可替代本文多数示例,但动态测试与参数化测试有本质区别

  • ❌ 动态测试不支持完整测试生命周期
  • ✅ 参数化测试支持完整生命周期

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

JUnit 5遵循扩展优于特性原则。因此,动态测试的主要目标是为第三方框架或扩展提供扩展点。

更多JUnit 5特性可参考重复测试文章。完整源码见GitHub仓库


原始标题:Guide to Dynamic Tests in Junit 5