JUnit 5 中的动态测试指南
上次更新:2024 年 1 月 8 日
1. 概述
动态测试是 JUnit 5 中引入的一种新的编程模型。 在本文中,我们将了解动态测试到底是什么以及如何创建它们。
如果您完全不熟悉 JUnit 5,您可能需要查看 JUnit 5 的预览版 和 我们的主要指南。
2. 什么是 DynamicTest?
使用 @Test 注解的标准测试是静态测试,这些测试在编译时完全指定。 DynamicTest 是在运行时生成的测试。 这些测试由带有 @TestFactory 注解的工厂方法生成。
@TestFactory 方法必须返回 Stream、Collection、Iterable 或 Iterator 类型的 DynamicTest 实例。 返回其他任何内容都会导致 JUnitException,因为无法在编译时检测到无效的返回类型。 除此之外,@TestFactory 方法不能是 static 或 private。
DynamicTests 的执行方式与标准 @Tests 不同,不支持生命周期回调。 也就是说,@BeforeEach 和 @AfterEach 方法不会为 DynamicTests 调用。
3. 创建 DynamicTests
首先,让我们看看创建 DynamicTests 的不同方法。
这里的示例本质上不是动态的,但它们将为创建真正动态的测试提供一个良好的起点。
我们将创建一个 Collection of 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 的 Collection。 每个 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)));
}
请注意,如果 @TestFactory 返回一个 Stream,则在执行完所有测试后,它将自动关闭。
输出将与第一个示例基本相同。 它将包含传递给动态测试的显示名称。
4. 创建 Stream of DynamicTests
为了演示目的,考虑一个 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...*/
}
这里除了我们已经熟悉的 @TestFactory 注解之外,没有太多与 DynamicTest 相关的代码。
这两个 ArrayList 将用作 DomainNameResolver 的输入和预期输出,分别。
现在让我们看看输入生成器
Iterator<String> inputGenerator = inputList.iterator();
输入生成器不过是 String 的 Iterator。 它使用我们的 inputList 并逐个返回域名。
显示名称生成器非常简单
Function<String, String> displayNameGenerator
= (input) -> "Resolving: " + input;
显示名称生成器的任务只是为测试用例提供一个显示名称,该名称将在 JUnit 报告或 IDE 的 JUnit 选项卡中使用。
在这里,我们只是利用域名为每个测试生成唯一的名称。 创建唯一名称不是必需的,但如果发生任何故障,这将有所帮助。 这样,我们将能够知道哪个域名导致测试用例失败。
现在让我们看一下我们测试的中心部分——测试执行代码
DomainNameResolver resolver = new DomainNameResolver();
ThrowingConsumer<String> 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. 使用 Java 8 功能改进DynamicTest
在前一节中编写的测试工厂可以通过使用 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() 的第一个参数是我们的显示名称生成器(“Resolving: ” + dom),而第二个参数,一个lambda,是我们的测试执行器。
输出与上一节中的输出相同。
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) 方法除了employeeId 之外还需要firstName。因此,它过滤掉了没有firstName 的Employee 实例。
最后,我们将两个流组合起来,并将所有测试作为单个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. 结论
参数化测试可以取代本文中的许多示例。但是,动态测试与参数化测试不同,因为它们不支持完整的测试生命周期,而参数化测试则支持。
此外,动态测试在如何生成输入和如何执行测试方面提供了更大的灵活性。
JUnit 5 倾向于扩展而非功能原则。因此,动态测试的主要目标是为第三方框架或扩展提供一个扩展点。
您可以在我们的关于 JUnit 5 中重复测试的文章中了解更多关于 JUnit 5 的其他功能。
支持本文的代码可在 GitHub 上获取。 一旦你以 Baeldung Pro 会员 身份登录,就开始学习并在项目上进行编码。















