动态测试
重要性:★★☆☆☆
注解为@Test
的测试方法是静态的,因为其行为是在编译时完全决定的,不能在运行时进行修改。Assumptions
(假设)提供了一定程度的动态行为(根据运行时环境条件动态调整测试执行)。
JUnit Jupiter提供了一种新的测试编程模型。可以通过注解了@TestFactory
的测试工厂方法在运行时生成动态测试。
技术上而言,@TestFactory
测试工厂方法必须返回单个的DynamicNode
实例,或由DynamicNode
组成的Stream,
Collection,
Iterable,
Iterator或数组。DynamicNode
有两个可实例化的子类:DynamicContainer
和DynamicTest
。DynamicContainer
包括一个显示名和一组动态的子节点,能够用来创建任意多层的嵌套动态节点树。DynamicTest
实例将被延迟执行,能够动态地甚至非确定性地生成测试用例。
@TestFactory
返回的任何流将由JUnit通过调用Stream.close()
正常关闭,因此使用类似Files.lines()
这样的资源是安全的。
跟@Test
方法一样,@TestFactory
方法也必须是非private、非静态的,可以包含能够通过参数解析器解析的参数。
DynamicTest
是由测试工厂方法在运行时生成的测试用例,它由一个显示名和一个Executable
组成。
与@Test
不同,生命周期方法是针对整个@TestFactory
测试工厂方法的,而不是针对测试工厂方法生成的每个测试用例。也就是说,模板方法生成的多个测试用例共享相同的测试类实例,一个测试用例修改了测试类的状态可能会影响下一个测试用例,因此必须小心对待。
下面是动态测试示例。
package yang.yu.tdd.dynamic;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
import yang.yu.tdd.Calculator;
import java.util.*;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static yang.yu.tdd.StringUtils.isPalindrome;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
class DynamicTestsDemo {
private final Calculator calculator = new Calculator();
// This will result in a JUnitException!
@TestFactory
List<String> dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
}
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertThat(isPalindrome("madam")).isTrue()),
dynamicTest("2nd dynamic test", () -> assertThat(calculator.multiply(2, 2)).isEqualTo(4))
);
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertThat(isPalindrome("madam")).isTrue()),
dynamicTest("4th dynamic test", () -> assertThat(calculator.multiply(2, 2)).isEqualTo(4))
);
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(
dynamicTest("5th dynamic test", () -> assertThat(isPalindrome("madam")).isTrue()),
dynamicTest("6th dynamic test", () -> assertThat(calculator.multiply(2, 2)).isEqualTo(4))
).iterator();
}
@TestFactory
DynamicTest[] dynamicTestsFromArray() {
return new DynamicTest[] {
dynamicTest("7th dynamic test", () -> assertThat(isPalindrome("madam")).isTrue()),
dynamicTest("8th dynamic test", () -> assertThat(calculator.multiply(2, 2)).isEqualTo(4))
};
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertThat(isPalindrome(text)).isTrue()));
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
// Generates tests for the first 10 even integers.
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> dynamicTest("test" + n, () -> assertThat(n % 2).isEqualTo(0)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTests() {
// Generates random positive integers between 0 and 100 until
// a number evenly divisible by 7 is encountered.
Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random();
int current;
@Override
public boolean hasNext() {
current = random.nextInt(100);
return current % 7 != 0;
}
@Override
public Integer next() {
return current;
}
};
// Generates display names like: input:5, input:37, input:85, etc.
Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
// Executes tests based on the current input value.
ThrowingConsumer<Integer> testExecutor = (input) -> assertThat(input % 7).isNotEqualTo(0);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of("A", "B", "C")
.map(input -> dynamicContainer("Container " + input, Stream.of(
dynamicTest("not null", () -> assertThat(input).isNotNull()),
dynamicContainer("properties", Stream.of(
dynamicTest("length > 0", () -> assertThat(input.length()).isPositive()),
dynamicTest("not empty", () -> assertThat(input).isNotEmpty())
))
)));
}
@TestFactory
DynamicNode dynamicNodeSingleTest() {
return dynamicTest("'pop' is a palindrome", () -> assertThat(isPalindrome("pop")).isTrue());
}
@TestFactory
DynamicNode dynamicNodeSingleContainer() {
return dynamicContainer("palindromes",
Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertThat(isPalindrome(text)).isTrue())
));
}
}