2. 概述
动态测试是JUnit 5引入的新编程模型。本文将深入探讨动态测试的本质及其创建方法。如果你对JUnit 5完全陌生,建议先阅读JUnit 5预览和核心指南。
3. 什么是DynamicTest?
标准测试使用@Test
注解标记,属于静态测试——在编译时完全确定。而**DynamicTest
是运行时生成的测试**,通过@TestFactory
注解的工厂方法创建。
@TestFactory
方法必须返回DynamicTest
实例的Stream
、Collection
、Iterable
或Iterator
。返回其他类型会抛出JUnitException
,因为无效返回类型在编译时无法检测。此外,@TestFactory
方法不能是static
或private
。
⚠️ 关键区别: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())
其他返回类型示例
相同逻辑可改用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)));
}
✅ 注意:返回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遵循扩展优于特性原则。因此,动态测试的主要目标是为第三方框架或扩展提供扩展点。