![0b27c2c631a46d239e662960c0a7f032.png](https://img-blog.csdnimg.cn/img_convert/0b27c2c631a46d239e662960c0a7f032.png)
动态测试
被@Test
注解标注的方法被认为是静态的,在编译时期它们就被确定了。而动态测试允许我们在代码运行时期产生测试用例。
在JUnit
5中引入了“动态测试(Dynamic tests
)”的概念,在动态测试中,允许我们通过工厂方法的方式批量生产测试用例,而这些产生的测试用例会被框架自动去执行,测试用例是在运行时动态产生的,而不是在编译时期,因而动态测试实例会被延迟执行。
@TestFactory
@TestFactory
是动态测试的核心注解,它用于标志一个方法为测试用例的工厂方法,而不是普通的测试方法,@TestFactory
方法本身不是测试用例,而是测试用例的工厂,它用来生产测试方法。如果没有理解也不要紧,看到实例或许更容易懂。
这个方法不能被private
和static
修饰,并且它必须有返回值,这也是它区别于普通测试方法的一个重要标志。
对于方法的返回值也是有要求的,它必须返回:Stream
、Collection
、Iterable
、Iterator
或是数组形式的DynamicNode
类型,对于方法返回的Stream
流(例如:Files.lines(),Files.list()
等)会被自动关闭,因而你无需手动关闭。
DynamicNode
DynamicNode
是个抽象类,有两个成员属性:
public abstract class DynamicNode {
// 生成的测试方法在测试视图展示的名称
private final String displayName;
// 测试方法可能用到的资源URI,可以是null
private final URI testSourceUri;
DynamicNode(String displayName, URI testSourceUri) {
this.displayName = Preconditions.notBlank(displayName, "displayName must not be null or blank");
this.testSourceUri = testSourceUri;
}
public String getDisplayName() {
return this.displayName;
}
public Optional<URI> getTestSourceUri() {
return Optional.ofNullable(testSourceUri);
}
@Override
public String toString() {
return new ToStringBuilder(this).append("displayName", displayName)
.append("testSourceUri", testSourceUri).toString();
}
}
这个抽象类有两个直接具体子类:DynamicTest
和DynamicContainer
。
DynamicTest
public class DynamicTest extends DynamicNode {
// 构造一个DynamicTest实例,可以把它认为就是一个测试用例了,displayName指定这个测试用例
// 的名称(会在测试视图节点树中显示),executable稍后讲解
public static DynamicTest dynamicTest(String displayName, Executable executable) {
return new DynamicTest(displayName, null, executable);
}
// 如果有额外需要的测试资源通过testSourceUri传入,java.net.URI
public static DynamicTest dynamicTest(String displayName, URI testSourceUri, Executable executable) {
return new DynamicTest(displayName, testSourceUri, executable);
}
// 将传入的inputGenerator构造成Stream流
public static <T> Stream<DynamicTest> stream(Iterator<T> inputGenerator,
Function<? super T, String> displayNameGenerator, ThrowingConsumer<? super T> testExecutor) {
Preconditions.notNull(inputGenerator, "inputGenerator must not be null");
Preconditions.notNull(displayNameGenerator, "displayNameGenerator must not be null");
Preconditions.notNull(testExecutor, "testExecutor must not be null");
// 将Iterator类型的inputGenerator通过java.util.Spliterators#spliteratorUnknownSize转换为Stream
return StreamSupport.stream(spliteratorUnknownSize(inputGenerator, ORDERED), false)
// 调用自身的静态方法dynamicTest(String displayName, Executable executable)
.map(input -> dynamicTest(displayNameGenerator.apply(input), () -> testExecutor.accept(input)));
}
// 产生的测试用例的具体执行逻辑
private final Executable executable;
private DynamicTest(String displayName, URI testSourceUri, Executable executable) {
super(displayName, testSourceUri);
this.executable = Preconditions.notNull(executable, "executable must not be null");
}
public Executable getExecutable() { return this.executable; }
}
再看Executable
类型,这是生成的测试用例具体的执行逻辑,可把它当成普通测试方法的方法体部分,它是函数式接口,因而支持lambda
表达式:
@FunctionalInterface
public interface Executable {
void execute() throws Throwable;
}
而org.junit.jupiter.api.function.ThrowingConsumer
也是函数式接口,类似于Java
8中的Consumer
,只不过它允许抛出异常:
@FunctionalInterface
public interface ThrowingConsumer<T> {
void accept(T t) throws Throwable;
}
看个简单示例:
@TestFactory
Stream<DynamicTest> dynamicTest() {
return IntStream.rangeClosed(1, 10)
.mapToObj(num -> DynamicTest.dynamicTest(
"exec - " + num, () -> System.out.println("任务: " + num)));
}
上面动态创建了10个测试用例,Executable
我使用了lambda
简化,方法返回的Stream<DynamicTest>
会被框架自动获取并执行(调用Executable
接口方法),这已经不是我们需要关心的事情:
![a0ae3b7793633a2d438368a30714e67f.png](https://img-blog.csdnimg.cn/img_convert/a0ae3b7793633a2d438368a30714e67f.png)
DynamicContainer
讲完了DynamicTest
,再看DynamicContainer
(前面讲过的部分不再重提):
public class DynamicContainer extends DynamicNode {
// 注意这里接收一个DynamicNode类型的Iterable,前面讲过DynamicNode有两个直接具体子类:
// DynamicTest和DynamicContainer,除了放DynamicTest还可放DynamicContainer,
// 这样可做到无限嵌套
public static DynamicContainer dynamicContainer(String displayName, Iterable<? extends DynamicNode> dynamicNodes) {
return dynamicContainer(displayName, null, StreamSupport.stream(dynamicNodes.spliterator(), false));
}
public static DynamicContainer dynamicContainer(String displayName, Stream<? extends DynamicNode> dynamicNodes) {
return dynamicContainer(displayName, null, dynamicNodes);
}
public static DynamicContainer dynamicContainer(String displayName, URI testSourceUri,
Stream<? extends DynamicNode> dynamicNodes) {
return new DynamicContainer(displayName, testSourceUri, dynamicNodes);
}
// 略
private final Stream<? extends DynamicNode> children;
private DynamicContainer(String displayName, URI testSourceUri, Stream<? extends DynamicNode> children) {
super(displayName, testSourceUri);
Preconditions.notNull(children, "children must not be null");
this.children = children;
}
public Stream<? extends DynamicNode> getChildren() { return children;
}
}
直接通过示例感受下:
@TestFactory
Stream<DynamicContainer> dynamicContainerStream() {
return IntStream.rangeClosed(1, 5)
.mapToObj(num -> {
// 将Executable接口实现提取出来,这样不用写两遍
Executable executable = () -> System.out.println(num);
// 测试用例
Stream<DynamicTest> dynamicTestStream = Arrays.stream(new DynamicTest[] {
DynamicTest.dynamicTest("test - " + num, executable),
DynamicTest.dynamicTest("test - " + num, executable)
});
return DynamicContainer.dynamicContainer("Outer container - " + num,
// 这里再嵌一层DynamicContainer
Stream.of(DynamicContainer.dynamicContainer(
"Inner container - " + num, dynamicTestStream))
);
});
}
运行结果:
![61e5d4e5c47bedc2a50ee0651a1a4358.png](https://img-blog.csdnimg.cn/img_convert/61e5d4e5c47bedc2a50ee0651a1a4358.png)
总的来说,这种使用DynamicContainer
嵌套的方式跟@Nested
很像。
软件版本
软件 | 版本 |
---|---|
JUnit | 5.6.2(junit-jupiter) |
结语
至此JUnit
5动态测试就说完了,要想深入理解还得结合源码和实例。透过源码可以发现,DynamicContainer
实质上并没有执行的入口,所以就像它的名称所宣示的那样,单纯就是个容器!最里层必须要有DynamicTest
来告知具体的测试逻辑。另外补充一点:动态测试方法不具备普通测试方法的生命周期,这意味着动态测试方法执行前后不会执行@BeforeEach
和@AfterEach
方法,不过@TestFactory
工厂方法本身除外。