[跟着官方文档学JUnit5][七][WritingTests][学习笔记]
1.测试模板
@TestTemplate方法不是常规的测试用例,而是测试用例的模板。因此,它被设计为根据注册提供者返回的调用上下文的数量多次调用。因此,它必须与已注册的TestTemplateInvocationContextProvider扩展一起使用。测试模板方法的每次调用都像执行常规@Test方法一样,完全支持相同的生命周期回调和扩展。重复测试和参数化测试是内置特殊的测试模板。
2.动态测试
JUnit Jupiter描述注解的@Test和JUnit 4中的@Test很相似。这些测试用例是静态的,因为它们是在编译时完全指定的,并且它们的行为不能被运行时发生的任何事情改变。 假设提供了动态行为的基本形式,但有意限制了它们的表达能力。
除了这些标准测试之外,JUnit Jupiter中还引入了一种全新的测试编程模型。这种新类型的测试是一种动态测试,它在运行时由带有@TestFactory注解的工厂方法生成。
与@Test方法相比,@TestFactory方法本身不是测试用例,而是测试用例的工厂。因此,动态测试是工厂的产品。从技术上讲,@TestFactory方法必须返回单个DynamicNode或Stream、Collection、Iterable、Iterator或DynamicNode实例数组。DynamicNode的可实例化子类是DynamicContainer和DynamicTest。DynamicContainer实例由显示名称和动态子节点列表组成,可以创建任意嵌套的动态节点层次结构。DynamicTest实例将被延迟执行,从而实现动态甚至非确定性的测试用例生成。
@TestFactory返回的任何Stream都将通过调用stream.close()正确关闭,从而可以安全地使用Files.lines()等资源。
与@Test方法一样,@TestFactory方法不能是私有的或静态的,并且可以选择声明要由ParameterResolvers解析的参数。
DynamicTest是在运行时生成的测试用例。它由显示名称和可执行文件组成。Executable是一个@FunctionalInterface,这意味着动态测试的实现可以作为lambda表达式或方法引用提供。
动态测试实例
以下DynamicTestsDemo类演示了测试工厂和动态测试的几个示例。
第一种方法返回无效的返回类型。 由于在编译时无法检测到无效的返回类型,因此在运行时检测到它时会抛出 JUnitException。
接下来的六个方法是非常简单的示例,演示了DynamicTest实例的Collection、Iterable、Iterator、数组或Stream的生成。这些示例中的大多数并没有真正展示动态行为,而只是展示了原则上支持的返回类型。但是,dynamicTestsFromStream()和dynamicTestsFromIntStream()展示了为给定的字符串集或输入数字范围生成动态测试是多么容易。
下一种方法本质上是真正动态的。generateRandomNumberOfTests()实现了一个生成随机数的迭代器、一个显示名称生成器和一个测试执行器,然后将所有三者提供给DynamicTest.stream()。尽管generateRandomNumberOfTests()的非确定性行为当然与测试可重复性相冲突,因此应谨慎使用,但它有助于展示动态测试的表现力。
next方法在灵活性方面类似于generateRandomNumberOfTests();但是,dynamicTestsFromStreamFactoryMethod()通过DynamicTest.stream()工厂方法从现有Stream生成动态测试流。
出于演示目的,dynamicNodeSingleTest()方法生成单个DynamicTest而不是流,dynamicNodeSingleContainer()方法使用DynamicContainer生成动态测试的嵌套层次结构。
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Named.named;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import com.example.util.Calculator;
import com.example.util.StringUtils;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.function.ThrowingConsumer;
import java.util.*;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
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.dynamicTest("1st dynamic test", () ->
assertTrue(StringUtils.isPalindrome("madam"))),
DynamicTest.dynamicTest("2nd dynamic test", () ->
assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
DynamicTest.dynamicTest("3rd dynamic test", () ->
assertTrue(StringUtils.isPalindrome("madam"))),
DynamicTest.dynamicTest("4th dynamic test", () ->
assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(
DynamicTest.dynamicTest("5th dynamic test", () ->
assertTrue(StringUtils.isPalindrome("madam"))),
DynamicTest.dynamicTest("6th dynamic test", () ->
assertEquals(4, calculator.multiply(2, 2)))
).iterator();
}
@TestFactory
DynamicTest[] dynamicTestsFromArray() {
return new DynamicTest[]{
DynamicTest.dynamicTest("7th dynamic test", () ->
assertTrue(StringUtils.isPalindrome("madam"))),
DynamicTest.dynamicTest("8th dynamic test", () ->
assertEquals(4, calculator.multiply(2, 2)))
};
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> DynamicTest.dynamicTest(text, () ->
assertTrue(StringUtils.isPalindrome(text))));
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
// Generate tests for the first 10 even integers.
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> DynamicTest.dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {
// Generate 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;
}
};
// Generate display names like: input:5, input:37, input:85, etc.
Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
// Execute tests based on the current input value.
ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
// Stream of palindromes to check
Stream<String> inputStream = Stream.of("racecar", "radar", "mom", "dad");
// Generate display names like: racecar is a palindrome
Function<String, String> displayNameGenerator = text -> text + " is a palindrome";
// Executes tests based on the current input value.
ThrowingConsumer<String> testExecutor = text -> assertTrue(StringUtils.isPalindrome(text));
//Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
//Stream of palindromes to check
Stream<Named<String>> inputStream = Stream.of(
named("racecar is a palindrome", "racecar"),
named("radar is also a palindrome", "radar"),
named("mom also seems to be a palindrome", "mom"),
named("dad is yet another palindrome", "dad")
);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream,
text -> assertTrue(StringUtils.isPalindrome(text)));
}
@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of("A", "B", "C")
.map(input -> dynamicContainer("Container " + input, Stream.of(
dynamicTest("not null", () -> assertNotNull(input)),
dynamicContainer("properties", Stream.of(
dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
))
)));
}
@TestFactory
DynamicNode dynamicNodeSingleTest() {
return dynamicTest("'pop' is a palindrome",
() -> assertTrue(StringUtils.isPalindrome("pop")));
}
@TestFactory
DynamicNode dynamicNodeSingleContainer() {
return dynamicContainer("palindromes",
Stream.of("racear", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () ->
assertTrue(StringUtils.isPalindrome(text))
)
)
);
}
}
动态测试的URI测试源
JUnit平台提供了TestSource,表示测试或容器的来源,用于通过IDE和构建工具导航到其位置。
动态测试或动态容器的TestSource可以从java.net.URI构造,该java.net.URI可以分别通过DynamicTest.dynamicTest(String, URI, Executable)或DynamicContainer.dynamicContainer(String, URI, Stream)工厂方法提供。URI将被转换为以下TestSource实现之一。
ClasspathResourceSource
如果URI包含classpath。例如:classpath:/test/foo.xml?line=20,column=2
DirectorySource
如果URI表示文件系统存在的目录
FileSource
如果URI表示文件系统存在的文件
MethodSource
如果URI包含方法和完全限定方法名称(FQMN),例如,method:org.junit.Foo#bar(java.lang.String,java.lang.String[])。
ClassSource
如果URI包含类和完全限定类名称,例如,class:org.junit.Foo?line=42
UriSource
如果上述TestSource实现均不适用。
3.超时
@Timeout注解允许人们声明如果测试、测试工厂、测试模板或生命周期方法的执行时间超过给定的持续时间,它应该失败。持续时间的时间单位默认为秒,但可配置。
以下示例显示了如何将@Timeout应用于生命周期和测试方法。
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import java.util.concurrent.TimeUnit;
class TimeoutDemo {
@BeforeEach
@Timeout(5)
void setUp() {
//fails if execution time exceeds 5 seconds
}
@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds100Milliseconds() {
// fails if execution time exceeds 100 milliseconds
}
}
与assertTimeoutPreemptively()断言相反,带注解的方法的执行在测试的主线程中进行。如果超时,主线程会被另一个线程中断。这样做是为了确保与Spring等框架的互操作性,这些框架使用对当前正在运行的线程敏感的机制——例如,ThreadLocal事务管理。
要将相同的超时应用于测试类及其所有@Nested类中的所有测试方法,可以在类级别声明@Timeout注解。然后它将应用于该类及其@Nested类中的所有测试、测试工厂和测试模板方法,除非被特定方法或@Nested类上的@Timeout注解覆盖。注意,在类级别声明的@Timeout注解不适用于生命周期方法。
在@TestFactory方法上声明@Timeout会检查工厂方法是否在指定的持续时间内返回,但不会验证工厂生成的每个单独DynamicTest的执行时间。请为此目的使用assertTimeout()或assertTimeoutPreemptively()。
如果@Timeout出现在@TestTemplate方法上,例如,@RepeatedTest或@ParameterizedTest,每次调用都将应用给定的超时。
以下配置参数可用于为某个类别的所有方法指定全局超时,除非它们或封闭的测试类使用@Timeout注解:
junit.jupiter.execution.timeout.default
所有可测试和生命周期方法的默认超时
junit.jupiter.execution.timeout.testable.method.default
所有可测试方法的默认超时
junit.jupiter.execution.timeout.test.method.default
@Test方法的默认超时
junit.jupiter.execution.timeout.testtemplate.method.default
@TestTemplate方法的默认超时
junit.jupiter.execution.timeout.testfactory.method.default
@TestFactory方法的默认超时
junit.jupiter.execution.timeout.lifecycle.method.default
所有生命周期方法的默认超时
junit.jupiter.execution.timeout.beforeall.method.default
@BeforeAll方法的默认超时
junit.jupiter.execution.timeout.beforeeach.method.default
@BeforeEach方法的默认超时
junit.jupiter.execution.timeout.aftereach.method.default
@AfterEach方法的默认超时
junit.jupiter.execution.timeout.afterall.method.default
@AfterAll方法的默认超时
更具体的配置参数会覆盖不太具体的配置参数。例如:
junit.jupiter.execution.timeout.test.method.default 重写
junit.jupiter.execution.timeout.testable.method.default 重写
junit.jupiter.execution.timeout.default
此类配置参数的值必须采用以下不区分大小写的格式:<number> [ns|μs|ms|s|m|h|d]
。数字和单位之间的空格可以省略。不指定单位相当于使用秒。以下示例为超时配置参数值。
参数值 | 等价注解 |
---|---|
42 | @Timeout(42) |
42 ns | @Timeout(value = 42, unit = NANOSECONDS) |
42 μs | @Timeout(value = 42, unit = MICROSECONDS) |
42 ms | @Timeout(value = 42, unit = MILLISECONDS) |
42 s | @Timeout(value = 42, unit = SECONDS) |
42 m | @Timeout(value = 42, unit = MINUTES) |
42 h | @Timeout(value = 42, unit = HOURS) |
42 d | @Timeout(value = 42, unit = DAYS) |
使用@Timeout进行轮询测试
在处理异步代码时,通常会编写在执行任何断言之前等待某事发生时轮询的测试。在某些情况下,可以重写逻辑以使用CountDownLatch或其他同步机制,但有时这是不可能的。例如,如果被测主体向外部消息代理中的通道发送消息,并且断言无法执行,直到消息已通过通道成功发送。像这样的异步测试需要某种形式的超时来确保它们不会因为无限期地执行而挂起测试套件,就像异步消息永远不会成功传递的情况一样。
通过为轮询的异步测试配置超时,可以确保测试不会无限期地执行。以下示例演示了如何使用JUnit Jupiter的@Timeout注解来实现这一点。这种技术可以很容易地用于实现“轮询直到”逻辑。
@Test
@Timeout(5) // Poll at most 5 seconds
void pollUntil() throws InterruptedException {
while (asynchronousResultNotAvailable()) {
Thread.sleep(250); // custom poll interval
}
// Obtain the asynchronous result and perform assertions
}
禁用全局@Timeout
在调试会话中单步执行代码时,固定的超时限制可能会影响测试结果,例如,尽管满足所有断言,但将测试标记为失败。
JUnit Jupiter支持junit.jupiter.execution.timeout.mode配置参数来配置何时应用超时。共有三种模式:启用、禁用和在debug时禁用。默认模式已启用。当其输入参数之一以-agentlib:jdwp开头时,VM运行时被视为在调试模式下运行。此启发式由disabled_on_debug模式查询。
4.并行执行
默认情况下,JUnit Jupiter测试在单个线程中按顺序运行。并行运行测试 可以加快执行速度,自5.3版起可作为可选功能使用。要启用并行执行,请将junit.jupiter.execution.parallel.enabled配置参数设置为true。例如,在junit-platform.properties中(有关其他选项,请参阅配置参数)。
请注意,启用此属性只是并行执行测试所需的第一步。 如果启用,默认情况下测试类和方法仍将按顺序执行。测试树中的节点是否并发执行由其执行模式控制。可以使用以下两种模式。
SAME_THREAD
强制在父级使用的同一线程中执行。例如,当用于测试方法时,该测试方法将与包含测试类的任何@BeforeAll或@AfterAll方法在同一线程中执行。
CONCURRENT
并发执行,除非资源锁定强制在同一线程中执行。
默认情况下,测试树中的节点使用SAME_THREAD执行模式。可以通过设置junit.jupiter.execution.parallel.mode.default配置参数来更改默认值。或者,可以使用@Execution注解来更改带注释的元素及其子元素(如果有)的执行模式,这允许您逐个激活各个测试类的并行执行。
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
默认执行模式应用于测试树的所有节点,但有一些值得注意的例外,即使用LifeCycle.per_class模式或MethodOderer的测试类(MethodOrderer.random除外)。在前一种情况下,测试作者必须确保测试类是线程安全的;在后者中,并发执行可能与已配置的执行顺序相抵触。 因此,在这两种情况下,仅在测试类或方法上存在@Exection(并发)注解时,仅同时执行此类测试类中的测试方法。
测试树中配置了并发执行模式的所有节点都将根据提供的配置完全并行执行,同时观察声明性同步机制。请注意,捕获标准输出/错误需要单独启用。
此外,还可以通过设置junit.jupiter.execution.parallel.mode.classes.default配置参数来配置顶级类的默认执行模式。通过组合这两个配置参数,您可以将类配置为并行运行,但其方法位于同一线程中:
配置参数,用于并行执行顶级类,但在同一线程中执行方法
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
相反的组合将并行运行一个类中的所有方法,但顶级类将按顺序运行:
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread
下图说明了对于junit.jupiter.execution.parallel.mode.default和junit.jupiter.execution.parallel.mode.classes.default这四个组合,执行两个顶级测试类A和B以及每个类两个测试方法的行为(请参阅第一列中的标签)。
默认执行模式配置组合
如果未显式设置junit.jupiter.execution.parallel.mode.classes.default配置参数,则将改用junit.jupiter.execution.parallel.mode.default的值。
4.1.配置
可以使用ParallelExecutionConfigurationStrategy配置所需的并行度和最大池大小等属性。JUnit平台提供了两种开箱即用的实现:dynamic实现和fixed实现。或者,可以实施自定义策略。
要选择策略,请将junit.jupiter.execution.parallel.config.strategy配置参数设置为以下选项之一。
dynamic
根据可用处理器/内核数乘以junit.jupiter.execution.parallel.config.dynamic.factor配置参数(默认为 1)计算所需的并行度。
fixed
使用必需的junit.jupiter.execution.parallel.config.fixed.parallelism配置参数作为所需的并行度。
custom
允许通过强制的junit.jupiter.execution.parallel.config.custom.class配置参数来指定自定义ParallelExecutionConfigurationStrategy实现,以确定所需的配置。
如果未设置配置策略,则JUnit Jupiter将使用系数为1的动态配置策略。因此,所需的并行度将等于可用处理器/内核的数量。
4.2.同步
除了使用@Execution注解控制执行模式之外,JUnit Jupiter还提供了另一种基于注解的声明性同步机制。@ResourceLock注解允许声明测试类或方法使用需要同步访问以确保可靠测试执行的特定共享资源。共享资源由唯一名称(字符串)标识。该名称可以是用户定义的,也可以是资源中的预定义常量之一:SYSTEM_PROPERTIES、SYSTEM_OUT、SYSTEM_ERR、LOCALE 或 TIME_ZONE。
如果以下示例中的测试在不使用@ResourceLock的情况下并行运行,则它们将是片状的。有时它们会通过,而在其他时候,由于写入然后读取相同的JVM系统属性的固有争用条件,它们会失败。
当使用@ResourceLock注解声明对共享资源的访问权限时,JUnit Jupiter引擎将使用此信息来确保不会并行运行冲突的测试。
除了唯一标识共享资源的字符串之外,还可以指定访问模式。需要对共享资源的READ访问权限的两个测试可以彼此并行运行,但当任何其他需要对同一共享资源READ_WRITE访问权限的测试运行时,则不会并行运行。
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.api.parallel.ResourceAccessMode;
import org.junit.jupiter.api.parallel.ResourceLock;
import java.util.Properties;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE;
import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES;
@Execution(ExecutionMode.CONCURRENT)
class SharedResourcesDemo {
private Properties backup;
@BeforeEach
void backup() {
backup = new Properties();
backup.putAll(System.getProperties());
}
@AfterEach
void restore() {
System.setProperties(backup);
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = ResourceAccessMode.READ)
void customPropertyIsNotSetByDefault() {
assertNull(System.getProperty("my.prop"));
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
void canSetCustomPropertyToApple() {
System.setProperty("my.prop", "apple");
assertEquals("apple", System.getProperty("my.prop"));
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
void canSetCustomPropertyToBanana() {
System.setProperty("my.prop", "banana");
assertEquals("banana", System.getProperty("my.prop"));
}
}
5.内置拓展
虽然JUnit团队鼓励在单独的库中打包和维护可重用的扩展,但JUnit Jupiter API工件包括一些面向用户的扩展实现,这些实现被认为非常有用,用户不必添加其他依赖项。
5.1.The TempDirectory Extension
内置的TempDirectory扩展用于为测试类中的单个测试或所有测试创建和清理临时目录。默认情况下,它处于注册状态。要使用它,请为@TempDir类型为java.nio.file.Path或java.io.File的字段添加注解,或者添加java.nio.file.Path或java.io.File类型的参数,这些参数用@TempDir注解为生命周期方法或测试方法。
例如,以下测试为单个测试方法声明了一个用@TempDir进行批注的参数,创建并写入临时目录中的文件,并检查其内容。
@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
可以通过指定多个带注解的参数来注入多个临时目录。
@Test
void copyFileFromSourceToTarget(@TempDir Path source, @TempDir Path target) throws IOException {
Path sourceFile = source.resolve("test.txt");
new ListWriter(sourceFile).write("a", "b", "c");
Path targetFile = Files.copy(sourceFile, target.resolve("test.txt"));
assertNotEquals(sourceFile, targetFile);
assertEquals(singletonList("a,b,c"), Files.readAllLines(targetFile));
}
构造函数参数不支持@TempDir。如果希望跨生命周期方法和当前测试方法保留对临时目录的单个引用,请使用字段注入,方法是使用@TempDir注释实例字段。
下面的示例将共享临时目录存储在静态字段中。这允许在测试类的所有生命周期方法和测试方法中使用相同的共享TempDir。为了更好地隔离,应使用实例字段,以便每个测试方法使用单独的目录。
class SharedTempDirectoryDemo {
@TempDir
static Path sharedTempDir;
@Test
void writeItemsToFile() throws IOException {
Path file = sharedTempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
@Test
void anotherTestThatUsesTheSameTempDir() {
// use sharedTempDir
}
}
- 点赞
- 收藏
- 关注作者
评论(0)