1. 概述
本文档旨在为编写测试的程序员、扩展作者和引擎作者以及构建工具和 IDE 供应商提供全面的参考文档。
本文档也可以作为 PDF 下载 获取。
1.1. 什么是 JUnit 5?
与 JUnit 的先前版本不同,JUnit 5 由来自三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit 平台 + JUnit Jupiter + JUnit Vintage
JUnit 平台是 在 JVM 上启动测试框架 的基础。它还定义了 TestEngine
API,用于开发在平台上运行的测试框架。此外,该平台还提供了一个 控制台启动器,用于从命令行启动平台,以及 JUnit 平台套件引擎,用于使用平台上的一个或多个测试引擎运行自定义测试套件。流行的 IDE(参见 IntelliJ IDEA、Eclipse、NetBeans 和 Visual Studio Code)和构建工具(参见 Gradle、Maven 和 Ant)也对 JUnit 平台提供了头等支持。
JUnit Jupiter 是 编程模型 和 扩展模型 的组合,用于在 JUnit 5 中编写测试和扩展。Jupiter 子项目为在平台上运行基于 Jupiter 的测试提供了一个 TestEngine
。
JUnit Vintage 提供了一个 TestEngine
,用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。它需要在类路径或模块路径上存在 JUnit 4.12 或更高版本。
1.3. 获取帮助
在 Stack Overflow 上询问与 JUnit 5 相关的问题,或在 Gitter 上与社区聊天。
1.4. 入门
1.4.3. 示例项目
要查看您可以复制和试验的完整工作示例项目,junit5-samples
存储库是一个不错的起点。junit5-samples
存储库托管着一组基于 JUnit Jupiter、JUnit Vintage 和其他测试框架的示例项目。您将在示例项目中找到相应的构建脚本(例如,build.gradle
、pom.xml
等)。以下链接突出显示了一些您可以选择的组合。
-
对于 Gradle 和 Java,请查看
junit5-jupiter-starter-gradle
项目。 -
对于 Gradle 和 Kotlin,请查看
junit5-jupiter-starter-gradle-kotlin
项目。 -
对于 Gradle 和 Groovy,请查看
junit5-jupiter-starter-gradle-groovy
项目。 -
对于 Maven,请查看
junit5-jupiter-starter-maven
项目。 -
对于 Ant,请查看
junit5-jupiter-starter-ant
项目。
2. 编写测试
以下示例简要介绍了在 JUnit Jupiter 中编写测试的最低要求。本章的后续部分将详细介绍所有可用功能。
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class MyFirstJUnitJupiterTests {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
2.1. 注解
JUnit Jupiter 支持以下注解来配置测试和扩展框架。
除非另有说明,所有核心注解都位于 org.junit.jupiter.api
包中,位于 junit-jupiter-api
模块中。
注解 | 描述 |
---|---|
|
表示一个方法是测试方法。与 JUnit 4 的 |
|
表示一个方法是 参数化测试。此类方法是 继承 的,除非它们被 覆盖。 |
|
表示一个方法是 重复测试 的测试模板。此类方法是 继承 的,除非它们被 覆盖。 |
|
表示一个方法是 动态测试 的测试工厂。此类方法是 继承 的,除非它们被 覆盖。 |
|
表示一个方法是 测试用例模板,旨在根据注册的 提供程序 返回的调用上下文数量多次调用。此类方法是 继承 的,除非它们被 覆盖。 |
|
用于配置注解测试类中 |
|
用于配置注解测试类的 测试方法执行顺序;类似于 JUnit 4 的 |
|
用于配置注解测试类的 测试实例生命周期。此类注解是 继承 的。 |
|
为测试类或测试方法声明一个自定义的 显示名称。此类注解不是 继承 的。 |
|
为测试类声明一个自定义的 显示名称生成器。此类注解是 继承 的。 |
|
表示应在当前类中 每个 |
|
表示应在当前类中 每个 |
|
表示应在当前类中 所有 |
|
表示应在当前类中 所有 |
|
表示注解类是 嵌套测试类。在 Java 8 到 Java 15 上, |
|
用于声明 用于过滤测试的标签,可以在类级别或方法级别使用;类似于 TestNG 中的测试组或 JUnit 4 中的类别。此类注解在类级别是 继承 的,但在方法级别不是。 |
|
用于 禁用 测试类或测试方法;类似于 JUnit 4 的 |
|
用于在测试、测试工厂、测试模板或生命周期方法的执行超过给定持续时间时使其失败。此类注解是 继承 的。 |
|
用于 声明式注册扩展。此类注解是 继承 的。 |
|
用于通过字段 以编程方式注册扩展。此类字段是 继承 的,除非它们被 遮蔽。 |
|
用于通过字段注入或参数注入在生命周期方法或测试方法中提供 临时目录;位于 |
某些注解目前可能处于 实验性 状态。有关详细信息,请参阅 实验性 API 中的表格。 |
2.1.1. 元注解和组合注解
JUnit Jupiter 注解可以用作 元注解。这意味着您可以定义自己的 组合注解,该注解将自动 继承 其元注解的语义。
例如,与其在整个代码库中复制粘贴 @Tag("fast")
(请参阅 标记和过滤),不如创建一个名为 @Fast
的自定义 组合注解。然后,@Fast
可以用作 @Tag("fast")
的直接替换。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}
以下 @Test
方法演示了 @Fast
注解的使用。
@Fast
@Test
void myFastTest() {
// ...
}
您甚至可以更进一步,引入一个自定义的 @FastTest
注解,该注解可以用作 @Tag("fast")
和 @Test
的直接替换。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}
JUnit 自动将以下内容识别为用“fast”标记的 @Test
方法。
@FastTest
void myFastTest() {
// ...
}
2.3. 测试类和方法
测试方法和生命周期方法可以在当前测试类中本地声明,从超类继承,或从接口继承(请参阅 测试接口和默认方法)。此外,测试方法和生命周期方法不能是 abstract
并且不能返回值(除了 @TestFactory
方法,它们需要返回值)。
类和方法可见性
测试类、测试方法和生命周期方法不需要是 通常建议省略测试类、测试方法和生命周期方法的 |
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class StandardTests {
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
}
@Test
void succeedingTest() {
}
@Test
void failingTest() {
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
2.4. 显示名称
测试类和测试方法可以通过 @DisplayName
声明自定义显示名称 - 包含空格、特殊字符甚至表情符号 - 这些名称将在测试报告以及测试运行器和 IDE 中显示。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("A special test case")
class DisplayNameDemo {
@Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() {
}
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("😱")
void testWithDisplayNameContainingEmoji() {
}
}
2.4.1. 显示名称生成器
JUnit Jupiter 支持自定义显示名称生成器,可以通过 @DisplayNameGeneration
注解进行配置。通过 @DisplayName
注解提供的值始终优先于 DisplayNameGenerator
生成的显示名称。
生成器可以通过实现 DisplayNameGenerator
来创建。以下是一些 Jupiter 中可用的默认生成器
DisplayNameGenerator | 行为 |
---|---|
|
与自 JUnit Jupiter 5.0 发布以来的标准显示名称生成行为匹配。 |
|
为没有参数的方法删除尾随括号。 |
|
将下划线替换为空格。 |
|
通过连接测试名称和封闭类的名称来生成完整的句子。 |
请注意,对于 IndicativeSentences
,您可以使用 @IndicativeSentencesGeneration
自定义分隔符和底层生成器,如下例所示。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.IndicativeSentencesGeneration;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
class DisplayNameGeneratorDemo {
@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_not_supported {
@Test
void if_it_is_zero() {
}
@DisplayName("A negative value for year is not supported by the leap year computation.")
@ParameterizedTest(name = "For example, year {0} is not supported.")
@ValueSource(ints = { -1, -4 })
void if_it_is_negative(int year) {
}
}
@Nested
@IndicativeSentencesGeneration(separator = " -> ", generator = ReplaceUnderscores.class)
class A_year_is_a_leap_year {
@Test
void if_it_is_divisible_by_4_but_not_by_100() {
}
@ParameterizedTest(name = "Year {0} is a leap year.")
@ValueSource(ints = { 2016, 2020, 2048 })
void if_it_is_one_of_the_following_years(int year) {
}
}
}
+-- DisplayNameGeneratorDemo [OK]
+-- A year is not supported [OK]
| +-- A negative value for year is not supported by the leap year computation. [OK]
| | +-- For example, year -1 is not supported. [OK]
| | '-- For example, year -4 is not supported. [OK]
| '-- if it is zero() [OK]
'-- A year is a leap year [OK]
+-- A year is a leap year -> if it is divisible by 4 but not by 100. [OK]
'-- A year is a leap year -> if it is one of the following years. [OK]
+-- Year 2016 is a leap year. [OK]
+-- Year 2020 is a leap year. [OK]
'-- Year 2048 is a leap year. [OK]
2.4.2. 设置默认显示名称生成器
您可以使用 junit.jupiter.displayname.generator.default
配置参数 指定要默认使用的 DisplayNameGenerator
的完全限定类名。与通过 @DisplayNameGeneration
注释配置的显示名称生成器一样,提供的类必须实现 DisplayNameGenerator
接口。默认显示名称生成器将用于所有测试,除非封闭测试类或测试接口上存在 @DisplayNameGeneration
注释。通过 @DisplayName
注释提供的值始终优先于 DisplayNameGenerator
生成的显示名称。
例如,要默认使用 ReplaceUnderscores
显示名称生成器,您应该将配置参数设置为相应的完全限定类名(例如,在 src/test/resources/junit-platform.properties
中)
junit.jupiter.displayname.generator.default = \
org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
同样,您可以指定实现 DisplayNameGenerator
的任何自定义类的完全限定名称。
总之,测试类或方法的显示名称根据以下优先级规则确定
-
如果存在,则为
@DisplayName
注释的值 -
如果存在,则通过调用
@DisplayNameGeneration
注释中指定的DisplayNameGenerator
-
如果存在,则通过调用通过配置参数配置的默认
DisplayNameGenerator
-
通过调用
org.junit.jupiter.api.DisplayNameGenerator.Standard
2.5. 断言
JUnit Jupiter 附带了 JUnit 4 的许多断言方法,并添加了一些适合与 Java 8 lambda 一起使用的方法。所有 JUnit Jupiter 断言都是 org.junit.jupiter.api.Assertions
类中的 static
方法。
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import example.domain.Person;
import example.util.Calculator;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
class AssertionsDemo {
private final Calculator calculator = new Calculator();
private final Person person = new Person("Jane", "Doe");
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and all
// failures will be reported together.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}
@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
new CountDownLatch(1).await();
});
}
private static String greeting() {
return "Hello, World!";
}
}
使用
assertTimeoutPreemptively() 进行抢占式超时
一个常见的例子是 Spring 框架中的事务测试支持。具体来说,Spring 的测试支持在调用测试方法之前将事务状态绑定到当前线程(通过 依赖于 |
2.5.1. Kotlin 断言支持
JUnit Jupiter 还附带了一些断言方法,这些方法非常适合在 Kotlin 中使用。所有 JUnit Jupiter Kotlin 断言都是 org.junit.jupiter.api
包中的顶级函数。
import example.domain.Person
import example.util.Calculator
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.assertTimeout
import org.junit.jupiter.api.assertTimeoutPreemptively
import java.time.Duration
class KotlinAssertionsDemo {
private val person = Person("Jane", "Doe")
private val people = setOf(person, Person("John", "Doe"))
@Test
fun `exception absence testing`() {
val calculator = Calculator()
val result = assertDoesNotThrow("Should not throw an exception") {
calculator.divide(0, 1)
}
assertEquals(0, result)
}
@Test
fun `expected exception testing`() {
val calculator = Calculator()
val exception = assertThrows<ArithmeticException> ("Should throw an exception") {
calculator.divide(1, 0)
}
assertEquals("/ by zero", exception.message)
}
@Test
fun `grouped assertions`() {
assertAll(
"Person properties",
{ assertEquals("Jane", person.firstName) },
{ assertEquals("Doe", person.lastName) }
)
}
@Test
fun `grouped assertions from a stream`() {
assertAll(
"People with first name starting with J",
people
.stream()
.map {
// This mapping returns Stream<() -> Unit>
{ assertTrue(it.firstName.startsWith("J")) }
}
)
}
@Test
fun `grouped assertions from a collection`() {
assertAll(
"People with last name of Doe",
people.map { { assertEquals("Doe", it.lastName) } }
)
}
@Test
fun `timeout not exceeded testing`() {
val fibonacciCalculator = FibonacciCalculator()
val result = assertTimeout(Duration.ofMillis(1000)) {
fibonacciCalculator.fib(14)
}
assertEquals(377, result)
}
@Test
fun `timeout exceeded with preemptive termination`() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(Duration.ofMillis(10)) {
// Simulate task that takes more than 10 ms.
Thread.sleep(100)
}
}
}
2.5.2. 第三方断言库
尽管 JUnit Jupiter 提供的断言功能足以满足许多测试场景,但有时需要更强大的功能和额外功能,例如匹配器。在这种情况下,JUnit 团队建议使用第三方断言库,例如 AssertJ、Hamcrest、Truth 等。因此,开发人员可以自由使用他们选择的断言库。
例如,匹配器和流畅 API 的组合可用于使断言更具描述性和可读性。但是,JUnit Jupiter 的 org.junit.jupiter.api.Assertions
类不提供像 JUnit 4 的 org.junit.Assert
类中找到的 assertThat()
方法,该方法接受 Hamcrest Matcher
。相反,鼓励开发人员使用第三方断言库提供的对匹配器的内置支持。
以下示例演示了如何在 JUnit Jupiter 测试中使用 Hamcrest 的 assertThat()
支持。只要 Hamcrest 库已添加到类路径中,您就可以静态导入 assertThat()
、is()
和 equalTo()
等方法,然后在测试中使用它们,就像下面的 assertWithHamcrestMatcher()
方法一样。
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class HamcrestAssertionsDemo {
private final Calculator calculator = new Calculator();
@Test
void assertWithHamcrestMatcher() {
assertThat(calculator.subtract(4, 1), is(equalTo(3)));
}
}
当然,基于 JUnit 4 编程模型的传统测试可以继续使用 org.junit.Assert#assertThat
。
2.6. 假设
JUnit Jupiter 附带了 JUnit 4 提供的假设方法的子集,并添加了一些适合与 Java 8 lambda 表达式和方法引用一起使用的方法。所有 JUnit Jupiter 假设都是 org.junit.jupiter.api.Assumptions
类中的静态方法。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class AssumptionsDemo {
private final Calculator calculator = new Calculator();
@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}
@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
}
@Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// perform these assertions only on the CI server
assertEquals(2, calculator.divide(4, 2));
});
// perform these assertions in all environments
assertEquals(42, calculator.multiply(6, 7));
}
}
从 JUnit Jupiter 5.4 开始,也可以使用 JUnit 4 的 org.junit.Assume 类中的方法进行假设。具体来说,JUnit Jupiter 支持 JUnit 4 的 AssumptionViolatedException 来表示应中止测试而不是将其标记为失败。 |
2.7. 禁用测试
可以通过 @Disabled
注释、通过 条件测试执行 中讨论的注释之一,或通过自定义 ExecutionCondition
来禁用整个测试类或单个测试方法。
这是一个 @Disabled
测试类。
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
这是一个包含 @Disabled
测试方法的测试类。
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class DisabledTestsDemo {
@Disabled("Disabled until bug #42 has been resolved")
@Test
void testWillBeSkipped() {
}
@Test
void testWillBeExecuted() {
}
}
|
|
2.8. 条件测试执行
JUnit Jupiter 中的 ExecutionCondition
扩展 API 允许开发人员以编程方式根据某些条件启用或禁用容器或测试。这种条件的最简单示例是内置的 DisabledCondition
,它支持 @Disabled
注释(请参阅 禁用测试)。除了 @Disabled
之外,JUnit Jupiter 还支持 org.junit.jupiter.api.condition
包中的一些其他基于注释的条件,这些条件允许开发人员声明式地启用或禁用容器和测试。当注册多个 ExecutionCondition
扩展时,只要其中一个条件返回禁用,容器或测试就会被禁用。如果您希望提供有关它们可能被禁用的详细信息,与这些内置条件关联的每个注释都具有一个 disabledReason
属性可用于此目的。
有关详细信息,请参阅 ExecutionCondition
和以下部分。
组合注释
请注意,以下部分列出的任何条件注释也可以用作元注释,以创建自定义组合注释。例如,@EnabledOnOs 演示 中的 |
JUnit Jupiter 中的条件注释不是 |
除非另有说明,否则以下部分列出的每个条件注释只能在给定的测试接口、测试类或测试方法上声明一次。如果条件注释直接存在、间接存在或元存在于给定元素上多次,则 JUnit 将只使用发现的第一个这样的注释;任何额外的声明将被静默忽略。但是请注意,每个条件注释都可以与 |
2.8.1. 操作系统和体系结构条件
容器或测试可以通过 @EnabledOnOs
和 @DisabledOnOs
注释在特定操作系统、体系结构或两者的组合上启用或禁用。
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
// ...
}
@TestOnMac
void testOnMac() {
// ...
}
@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
// ...
}
@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
// ...
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}
@Test
@EnabledOnOs(architectures = "aarch64")
void onAarch64() {
// ...
}
@Test
@DisabledOnOs(architectures = "x86_64")
void notOnX86_64() {
// ...
}
@Test
@EnabledOnOs(value = MAC, architectures = "aarch64")
void onNewMacs() {
// ...
}
@Test
@DisabledOnOs(value = MAC, architectures = "aarch64")
void notOnNewMacs() {
// ...
}
2.8.2. Java 运行时环境条件
容器或测试可以通过 @EnabledOnJre
和 @DisabledOnJre
注释在 Java 运行时环境 (JRE) 的特定版本上启用或禁用,或者可以通过 @EnabledForJreRange
和 @DisabledForJreRange
注释在 JRE 的特定版本范围内启用或禁用。范围默认为 JRE.JAVA_8
作为下边界 (min
) 和 JRE.OTHER
作为上边界 (max
),这允许使用半开范围。
@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
// ...
}
@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9)
void fromJava9toCurrentJavaFeatureNumber() {
// ...
}
@Test
@EnabledForJreRange(max = JAVA_11)
void fromJava8To11() {
// ...
}
@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
// ...
}
@Test
@DisabledForJreRange(min = JAVA_9, max = JAVA_11)
void notFromJava9to11() {
// ...
}
@Test
@DisabledForJreRange(min = JAVA_9)
void notFromJava9toCurrentJavaFeatureNumber() {
// ...
}
@Test
@DisabledForJreRange(max = JAVA_11)
void notFromJava8to11() {
// ...
}
2.8.3. 原生映像条件
容器或测试可以通过 @EnabledInNativeImage
和 @DisabledInNativeImage
注释在 GraalVM 原生映像 中启用或禁用。这些注释通常用于在使用 GraalVM 原生构建工具 项目中的 Gradle 和 Maven 插件在原生映像中运行测试时使用。
@Test
@EnabledInNativeImage
void onlyWithinNativeImage() {
// ...
}
@Test
@DisabledInNativeImage
void neverWithinNativeImage() {
// ...
}
2.8.4. 系统属性条件
根据named
JVM 系统属性的值,可以使用@EnabledIfSystemProperty
和 @DisabledIfSystemProperty
注解启用或禁用容器或测试。通过matches
属性提供的值将被解释为正则表达式。
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
// ...
}
@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
// ...
}
从 JUnit Jupiter 5.6 开始, |
2.8.5. 环境变量条件
可以使用@EnabledIfEnvironmentVariable
和 @DisabledIfEnvironmentVariable
注解,根据底层操作系统的named
环境变量的值启用或禁用容器或测试。通过matches
属性提供的值将被解释为正则表达式。
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
// ...
}
@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
// ...
}
从 JUnit Jupiter 5.6 开始, |
2.8.6. 自定义条件
作为实现 ExecutionCondition
的替代方案,可以使用通过@EnabledIf
和 @DisabledIf
注解配置的条件方法启用或禁用容器或测试。条件方法必须具有boolean
返回类型,并且可以不接受参数,也可以接受单个ExtensionContext
参数。
以下测试类演示了如何通过@EnabledIf
和 @DisabledIf
配置名为customCondition
的本地方法。
@Test
@EnabledIf("customCondition")
void enabled() {
// ...
}
@Test
@DisabledIf("customCondition")
void disabled() {
// ...
}
boolean customCondition() {
return true;
}
或者,条件方法可以位于测试类之外。在这种情况下,它必须通过其完全限定名称引用,如以下示例所示。
package example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
class ExternalCustomConditionDemo {
@Test
@EnabledIf("example.ExternalCondition#customCondition")
void enabled() {
// ...
}
}
class ExternalCondition {
static boolean customCondition() {
return true;
}
}
在以下几种情况下,条件方法需要是
在任何其他情况下,可以使用静态方法或实例方法作为条件方法。 |
通常,可以使用实用程序类中的现有静态方法作为自定义条件。 例如,
|
2.9. 标记和过滤
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
@Tag("model")
class TaggingDemo {
@Test
@Tag("taxes")
void testingTaxCalculation() {
}
}
有关演示如何为标记创建自定义注解的示例,请参阅 元注解和组合注解。 |
2.10. 测试执行顺序
默认情况下,测试类和方法将使用确定性但故意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行测试类和测试方法,从而允许可重复构建。
有关测试方法和测试类的定义,请参阅 定义。 |
2.10.1. 方法顺序
虽然真正的单元测试通常不应该依赖于执行的顺序,但在某些情况下需要强制执行特定的测试方法执行顺序,例如,在编写集成测试或功能测试时,测试的顺序很重要,尤其是在与@TestInstance(Lifecycle.PER_CLASS)
结合使用时。
要控制测试方法的执行顺序,请使用@TestMethodOrder
注解您的测试类或测试接口,并指定所需的MethodOrderer
实现。您可以实现自己的自定义MethodOrderer
或使用以下内置MethodOrderer
实现之一。
-
MethodOrderer.DisplayName
:根据测试方法的显示名称(请参阅 显示名称生成优先级规则)以字母数字顺序对测试方法进行排序。 -
MethodOrderer.MethodName
:根据测试方法的名称和形式参数列表以字母数字顺序对测试方法进行排序。 -
MethodOrderer.OrderAnnotation
:根据通过@Order
注解指定的值以数字顺序对测试方法进行排序。 -
MethodOrderer.Random
:以伪随机方式对测试方法进行排序,并支持自定义种子的配置。 -
MethodOrderer.Alphanumeric
:根据测试方法的名称和形式参数列表以字母数字顺序对测试方法进行排序;在 6.0 中弃用,以支持MethodOrderer.MethodName
,将在 6.0 中删除。
另请参阅:回调的包装行为 |
以下示例演示了如何保证测试方法按通过@Order
注解指定的顺序执行。
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {
@Test
@Order(1)
void nullValues() {
// perform assertions against null values
}
@Test
@Order(2)
void emptyValues() {
// perform assertions against empty values
}
@Test
@Order(3)
void validValues() {
// perform assertions against valid values
}
}
设置默认方法排序器
您可以使用junit.jupiter.testmethod.order.default
配置参数 指定要默认使用的MethodOrderer
的完全限定类名。与通过@TestMethodOrder
注解配置的排序器一样,提供的类必须实现MethodOrderer
接口。默认排序器将用于所有测试,除非在封闭的测试类或测试接口上存在@TestMethodOrder
注解。
例如,要默认使用MethodOrderer.OrderAnnotation
方法排序器,您应该将配置参数设置为相应的完全限定类名(例如,在src/test/resources/junit-platform.properties
中)。
junit.jupiter.testmethod.order.default = \
org.junit.jupiter.api.MethodOrderer$OrderAnnotation
同样,您可以指定实现MethodOrderer
的任何自定义类的完全限定名称。
2.10.2. 类顺序
虽然测试类通常不应该依赖于执行的顺序,但在某些情况下,需要强制执行特定的测试类执行顺序。您可能希望以随机顺序执行测试类,以确保测试类之间没有意外的依赖关系,或者您可能希望对测试类进行排序以优化构建时间,如以下场景中所述。
-
首先运行之前失败的测试和更快的测试:“快速失败”模式
-
在启用并行执行的情况下,首先安排较长的测试:“最短测试计划执行时间”模式
-
各种其他用例
要为整个测试套件全局配置测试类执行顺序,请使用junit.jupiter.testclass.order.default
配置参数 指定要使用的ClassOrderer
的完全限定类名。提供的类必须实现ClassOrderer
接口。
您可以实现自己的自定义ClassOrderer
或使用以下内置ClassOrderer
实现之一。
-
ClassOrderer.ClassName
:根据测试类的完全限定类名以字母数字顺序对测试类进行排序。 -
ClassOrderer.DisplayName
:根据测试类的显示名称(请参阅 显示名称生成优先级规则)以字母数字顺序对测试类进行排序。 -
ClassOrderer.OrderAnnotation
:根据通过@Order
注解指定的值以数字顺序对测试类进行排序。 -
ClassOrderer.Random
:以伪随机方式对测试类进行排序,并支持自定义种子的配置。
例如,要使@Order
注解在测试类上生效,您应该使用配置参数和相应的完全限定类名(例如,在src/test/resources/junit-platform.properties
中)配置ClassOrderer.OrderAnnotation
类排序器。
junit.jupiter.testclass.order.default = \
org.junit.jupiter.api.ClassOrderer$OrderAnnotation
配置的ClassOrderer
将应用于所有顶级测试类(包括static
嵌套测试类)和@Nested
测试类。
顶级测试类将相对于彼此进行排序;而@Nested 测试类将相对于共享相同封闭类的其他@Nested 测试类进行排序。 |
要为@Nested
测试类本地配置测试类执行顺序,请在要排序的@Nested
测试类的封闭类上声明@TestClassOrder
注解,并在@TestClassOrder
注解中直接提供要使用的ClassOrderer
实现的类引用。配置的ClassOrderer
将递归应用于@Nested
测试类及其@Nested
测试类。请注意,本地@TestClassOrder
声明始终会覆盖继承的@TestClassOrder
声明或通过junit.jupiter.testclass.order.default
配置参数全局配置的ClassOrderer
。
以下示例演示了如何保证@Nested
测试类按通过@Order
注解指定的顺序执行。
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class OrderedNestedTestClassesDemo {
@Nested
@Order(1)
class PrimaryTests {
@Test
void test1() {
}
}
@Nested
@Order(2)
class SecondaryTests {
@Test
void test2() {
}
}
}
2.11. 测试实例生命周期
为了允许单独的测试方法以隔离方式执行,并避免由于可变测试实例状态而导致的意外副作用,JUnit 在执行每个测试方法(请参阅 定义)之前会创建每个测试类的新实例。这种“每方法”测试实例生命周期是 JUnit Jupiter 中的默认行为,与 JUnit 的所有先前版本类似。
请注意,即使在“每方法”测试实例生命周期模式处于活动状态时,如果给定的测试方法通过 条件(例如,@Disabled 、@DisabledOnOs 等)被禁用,测试类仍将被实例化。 |
如果您希望 JUnit Jupiter 在同一个测试实例上执行所有测试方法,请使用 @TestInstance(Lifecycle.PER_CLASS)
注解您的测试类。使用此模式时,将为每个测试类创建一个新的测试实例。因此,如果您的测试方法依赖于存储在实例变量中的状态,您可能需要在 @BeforeEach
或 @AfterEach
方法中重置该状态。
“per-class” 模式比默认的“per-method” 模式有一些额外的优势。具体来说,使用“per-class” 模式,可以将 @BeforeAll
和 @AfterAll
声明在非静态方法以及接口 default
方法上。因此,“per-class” 模式也使得在 @Nested
测试类中使用 @BeforeAll
和 @AfterAll
方法成为可能。
从 Java 16 开始,@BeforeAll 和 @AfterAll 方法可以在 @Nested 测试类中声明为 static 。 |
如果您使用 Kotlin 编程语言编写测试,您可能还会发现通过切换到“per-class” 测试实例生命周期模式,更容易实现非静态 @BeforeAll
和 @AfterAll
生命周期方法以及 @MethodSource
工厂方法。
2.11.1. 更改默认测试实例生命周期
如果测试类或测试接口没有使用 @TestInstance
注解,JUnit Jupiter 将使用默认生命周期模式。标准默认模式是 PER_METHOD
;但是,可以更改整个测试计划执行的默认模式。要更改默认测试实例生命周期模式,请将 junit.jupiter.testinstance.lifecycle.default
配置参数设置为 TestInstance.Lifecycle
中定义的枚举常量的名称,不区分大小写。这可以作为 JVM 系统属性提供,作为传递给 Launcher
的 LauncherDiscoveryRequest
中的配置参数,或者通过 JUnit Platform 配置文件提供(有关详细信息,请参阅 配置参数)。
例如,要将默认测试实例生命周期模式设置为 Lifecycle.PER_CLASS
,您可以使用以下系统属性启动 JVM。
-Djunit.jupiter.testinstance.lifecycle.default=per_class
但是请注意,通过 JUnit Platform 配置文件设置默认测试实例生命周期模式是一种更稳健的解决方案,因为配置文件可以与您的项目一起签入版本控制系统,因此可以在 IDE 和构建软件中使用。
要通过 JUnit Platform 配置文件将默认测试实例生命周期模式设置为 Lifecycle.PER_CLASS
,请在类路径的根目录(例如,src/test/resources
)中创建一个名为 junit-platform.properties
的文件,内容如下。
junit.jupiter.testinstance.lifecycle.default = per_class
如果更改默认测试实例生命周期模式,但未一致应用,则会导致不可预测的结果和脆弱的构建。例如,如果构建配置“per-class” 语义作为默认值,但在 IDE 中执行测试时使用“per-method” 语义,这可能会导致难以调试在构建服务器上发生的错误。因此,建议在 JUnit Platform 配置文件中更改默认值,而不是通过 JVM 系统属性更改默认值。 |
2.12. 嵌套测试
@Nested
测试为测试编写者提供了更多功能来表达几个测试组之间的关系。此类嵌套测试利用了 Java 的嵌套类,并促进了对测试结构的分层思考。以下是一个详细的示例,包括源代码和在 IDE 中执行的屏幕截图。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
在 IDE 中执行此示例时,GUI 中的测试执行树将类似于以下图像。
在此示例中,通过为设置代码定义分层生命周期方法,外部测试中的先决条件在内部测试中使用。例如,createNewStack()
是一个 @BeforeEach
生命周期方法,它在定义它的测试类中使用,并在定义它的类下方嵌套树的所有级别中使用。
外部测试的设置代码在内部测试执行之前运行的事实使您能够独立运行所有测试。您甚至可以单独运行内部测试,而无需运行外部测试,因为外部测试的设置代码始终会执行。
只有非静态嵌套类(即内部类)可以作为 @Nested 测试类。嵌套可以是任意深度的,这些内部类都支持完整的生命周期支持,只有一个例外:@BeforeAll 和 @AfterAll 方法默认不起作用。原因是 Java 在 Java 16 之前不允许内部类中的 static 成员。但是,可以通过使用 @TestInstance(Lifecycle.PER_CLASS) 注解 @Nested 测试类来规避此限制(请参阅 测试实例生命周期)。如果您使用的是 Java 16 或更高版本,则可以在 @Nested 测试类中将 @BeforeAll 和 @AfterAll 方法声明为 static ,并且此限制不再适用。 |
2.13. 构造函数和方法的依赖注入
在所有以前的 JUnit 版本中,测试构造函数或方法不允许具有参数(至少在标准 Runner
实现中不允许)。作为 JUnit Jupiter 的主要更改之一,测试构造函数和方法现在都允许具有参数。这允许更大的灵活性,并为构造函数和方法启用依赖注入。
ParameterResolver
定义了希望在运行时动态解析参数的测试扩展的 API。如果测试类构造函数、测试方法或生命周期方法(请参阅 定义)接受参数,则该参数必须在运行时由注册的 ParameterResolver
解析。
目前有三个内置解析器会自动注册。
-
TestInfoParameterResolver
:如果构造函数或方法参数的类型为TestInfo
,则TestInfoParameterResolver
将提供一个TestInfo
实例,该实例对应于当前容器或测试,作为参数的值。然后可以使用TestInfo
来检索有关当前容器或测试的信息,例如显示名称、测试类、测试方法和关联的标签。显示名称要么是技术名称(例如测试类或测试方法的名称),要么是通过@DisplayName
配置的自定义名称。TestInfo
充当 JUnit 4 中TestName
规则的直接替代。以下演示了如何将TestInfo
注入测试构造函数、@BeforeEach
方法和@Test
方法。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
@DisplayName("TestInfo Demo")
class TestInfoDemo {
TestInfoDemo(TestInfo testInfo) {
assertEquals("TestInfo Demo", testInfo.getDisplayName());
}
@BeforeEach
void init(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}
@Test
@DisplayName("TEST 1")
@Tag("my-tag")
void test1(TestInfo testInfo) {
assertEquals("TEST 1", testInfo.getDisplayName());
assertTrue(testInfo.getTags().contains("my-tag"));
}
@Test
void test2() {
}
}
-
RepetitionExtension
:如果@RepeatedTest
、@BeforeEach
或@AfterEach
方法中的方法参数的类型为RepetitionInfo
,则RepetitionExtension
将提供一个RepetitionInfo
实例。然后可以使用RepetitionInfo
来检索有关当前重复、总重复次数、已失败的重复次数以及对应@RepeatedTest
的失败阈值的信息。但是请注意,RepetitionExtension
不会在@RepeatedTest
的上下文中之外注册。请参阅 重复测试示例。 -
TestReporterParameterResolver
:如果构造函数或方法参数的类型为TestReporter
,则TestReporterParameterResolver
将提供一个TestReporter
实例。TestReporter
可用于发布有关当前测试运行的附加数据。可以通过TestExecutionListener
中的reportingEntryPublished()
方法使用这些数据,从而允许在 IDE 中查看这些数据或将其包含在报告中。在 JUnit Jupiter 中,您应该使用
TestReporter
,就像您以前在 JUnit 4 中向stdout
或stderr
打印信息一样。使用@RunWith(JUnitPlatform.class)
将把所有报告的条目输出到stdout
。此外,一些 IDE 会将报告条目打印到stdout
或在测试结果的用户界面中显示它们。
class TestReporterDemo {
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a status message");
}
@Test
void reportKeyValuePair(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
@Test
void reportMultipleKeyValuePairs(TestReporter testReporter) {
Map<String, String> values = new HashMap<>();
values.put("user name", "dk38");
values.put("award year", "1974");
testReporter.publishEntry(values);
}
}
其他参数解析器必须通过 @ExtendWith 注册适当的 扩展 来显式启用。 |
查看 RandomParametersExtension
以了解自定义 ParameterResolver
的示例。虽然它并非旨在用于生产环境,但它演示了扩展模型和参数解析过程的简单性和表现力。MyRandomParametersTest
演示了如何将随机值注入 @Test
方法。
@ExtendWith(RandomParametersExtension.class)
class MyRandomParametersTest {
@Test
void injectsInteger(@Random int i, @Random int j) {
assertNotEquals(i, j);
}
@Test
void injectsDouble(@Random double d) {
assertEquals(0.0, d, 1.0);
}
}
对于实际用例,请查看 MockitoExtension
和 SpringExtension
的源代码。
当要注入的参数的类型是 ParameterResolver
的唯一条件时,您可以使用通用的 TypeBasedParameterResolver
基类。supportsParameters
方法在幕后实现,并支持参数化类型。
2.14. 测试接口和默认方法
JUnit Jupiter 允许在接口 default
方法上声明 @Test
、@RepeatedTest
、@ParameterizedTest
、@TestFactory
、@TestTemplate
、@BeforeEach
和 @AfterEach
。@BeforeAll
和 @AfterAll
可以声明在测试接口中的 static
方法上,也可以声明在接口 default
方法上,前提是测试接口或测试类使用 @TestInstance(Lifecycle.PER_CLASS)
注解(请参阅 测试实例生命周期)。以下是一些示例。
@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {
static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());
@BeforeAll
default void beforeAllTests() {
logger.info("Before all tests");
}
@AfterAll
default void afterAllTests() {
logger.info("After all tests");
}
@BeforeEach
default void beforeEachTest(TestInfo testInfo) {
logger.info(() -> String.format("About to execute [%s]",
testInfo.getDisplayName()));
}
@AfterEach
default void afterEachTest(TestInfo testInfo) {
logger.info(() -> String.format("Finished executing [%s]",
testInfo.getDisplayName()));
}
}
interface TestInterfaceDynamicTestsDemo {
@TestFactory
default Stream<DynamicTest> dynamicTestsForPalindromes() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
}
@ExtendWith
和 @Tag
可以声明在测试接口上,以便实现该接口的类自动继承其标签和扩展。请参阅 测试执行回调之前和之后,了解 TimingExtension 的源代码。
@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}
然后,您可以在测试类中实现这些测试接口,以使它们应用。
class TestInterfaceDemo implements TestLifecycleLogger,
TimeExecutionLogger, TestInterfaceDynamicTestsDemo {
@Test
void isEqualValue() {
assertEquals(1, "a".length(), "is always equal");
}
}
运行 TestInterfaceDemo
会产生类似于以下的输出
INFO example.TestLifecycleLogger - Before all tests INFO example.TestLifecycleLogger - About to execute [dynamicTestsForPalindromes()] INFO example.TimingExtension - Method [dynamicTestsForPalindromes] took 19 ms. INFO example.TestLifecycleLogger - Finished executing [dynamicTestsForPalindromes()] INFO example.TestLifecycleLogger - About to execute [isEqualValue()] INFO example.TimingExtension - Method [isEqualValue] took 1 ms. INFO example.TestLifecycleLogger - Finished executing [isEqualValue()] INFO example.TestLifecycleLogger - After all tests
此功能的另一个可能的应用是为接口契约编写测试。例如,您可以为 Object.equals
或 Comparable.compareTo
的实现如何表现编写测试,如下所示。
public interface Testable<T> {
T createValue();
}
public interface EqualsContract<T> extends Testable<T> {
T createNotEqualValue();
@Test
default void valueEqualsItself() {
T value = createValue();
assertEquals(value, value);
}
@Test
default void valueDoesNotEqualNull() {
T value = createValue();
assertFalse(value.equals(null));
}
@Test
default void valueDoesNotEqualDifferentValue() {
T value = createValue();
T differentValue = createNotEqualValue();
assertNotEquals(value, differentValue);
assertNotEquals(differentValue, value);
}
}
public interface ComparableContract<T extends Comparable<T>> extends Testable<T> {
T createSmallerValue();
@Test
default void returnsZeroWhenComparedToItself() {
T value = createValue();
assertEquals(0, value.compareTo(value));
}
@Test
default void returnsPositiveNumberWhenComparedToSmallerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(value.compareTo(smallerValue) > 0);
}
@Test
default void returnsNegativeNumberWhenComparedToLargerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(smallerValue.compareTo(value) < 0);
}
}
然后,您可以在测试类中实现这两个契约接口,从而继承相应的测试。当然,您必须实现抽象方法。
class StringTests implements ComparableContract<String>, EqualsContract<String> {
@Override
public String createValue() {
return "banana";
}
@Override
public String createSmallerValue() {
return "apple"; // 'a' < 'b' in "banana"
}
@Override
public String createNotEqualValue() {
return "cherry";
}
}
上面的测试仅仅是作为示例,因此并不完整。 |
2.15. 重复测试
JUnit Jupiter 提供了通过使用 `@RepeatedTest` 注解方法并指定所需的总重复次数来重复测试指定次数的功能。每次重复测试的调用都类似于执行一个普通的 `@Test` 方法,完全支持相同的生命周期回调和扩展。
以下示例演示了如何声明一个名为 `repeatedTest()` 的测试,该测试将自动重复 10 次。
@RepeatedTest(10)
void repeatedTest() {
// ...
}
从 JUnit Jupiter 5.10 开始,`@RepeatedTest` 可以配置一个失败阈值,该阈值表示在超过该阈值后,剩余的重复将自动跳过。将 `failureThreshold` 属性设置为小于总重复次数的正数,以便在遇到指定次数的失败后跳过剩余重复的调用。
例如,如果您使用 `@RepeatedTest` 来重复调用一个您怀疑可能不稳定的测试,那么一次失败就足以证明该测试不稳定,并且没有必要调用剩余的重复。为了支持这种特定用例,请将 `failureThreshold` 设置为 `1`。您也可以根据您的用例将阈值设置为大于 1 的数字。
默认情况下,`failureThreshold` 属性设置为 `Integer.MAX_VALUE`,表示不会应用任何失败阈值,这实际上意味着无论任何重复是否失败,都将调用指定的重复次数。
如果 `@RepeatedTest` 方法的重复是并行执行的,则无法保证失败阈值。因此,建议在配置并行执行时,使用 `@Execution(SAME_THREAD)` 注解 `@RepeatedTest` 方法。有关更多详细信息,请参阅 并行执行。 |
除了指定重复次数和失败阈值之外,还可以通过 `@RepeatedTest` 注解的 `name` 属性为每次重复配置一个自定义显示名称。此外,显示名称可以是静态文本和动态占位符组合的模式。目前支持以下占位符。
-
{displayName}
: `@RepeatedTest` 方法的显示名称 -
{currentRepetition}
: 当前重复次数 -
{totalRepetitions}
: 总重复次数
给定重复的默认显示名称是根据以下模式生成的:"repetition {currentRepetition} of {totalRepetitions}"
。因此,先前 `repeatedTest()` 示例中各个重复的显示名称将是:repetition 1 of 10
、repetition 2 of 10
等。如果您希望 `@RepeatedTest` 方法的显示名称包含在每次重复的名称中,您可以定义自己的自定义模式或使用预定义的 `RepeatedTest.LONG_DISPLAY_NAME` 模式。后者等于 "{displayName} :: repetition {currentRepetition} of {totalRepetitions}"
,这将导致各个重复的显示名称类似于 repeatedTest() :: repetition 1 of 10
、repeatedTest() :: repetition 2 of 10
等。
为了检索有关当前重复、总重复次数、已失败的重复次数和失败阈值的信息,开发人员可以选择将 `RepetitionInfo` 实例注入到 `@RepeatedTest`、`@BeforeEach` 或 `@AfterEach` 方法中。
2.15.1. 重复测试示例
本节末尾的 `RepeatedTestsDemo` 类演示了几个重复测试的示例。
repeatedTest()
方法与上一节中的示例相同;而 `repeatedTestWithRepetitionInfo()` 演示了如何将 `RepetitionInfo` 实例注入测试以访问当前重复测试的总重复次数。
repeatedTestWithFailureThreshold()
演示了如何设置失败阈值,并模拟了每隔一次重复的意外失败。最终的行为可以在本节末尾的 `ConsoleLauncher` 输出中查看。
接下来的两个方法演示了如何在每次重复的显示名称中包含 `@RepeatedTest` 方法的自定义 `@DisplayName`。`customDisplayName()` 将自定义显示名称与自定义模式组合在一起,然后使用 `TestInfo` 验证生成的显示名称的格式。`Repeat!` 是来自 `@DisplayName` 声明的 `{displayName}`,而 `1/1` 来自 `{currentRepetition}/{totalRepetitions}`。相反,`customDisplayNameWithLongPattern()` 使用了前面提到的预定义 `RepeatedTest.LONG_DISPLAY_NAME` 模式。
repeatedTestInGerman()
演示了将重复测试的显示名称翻译成外语(在本例中为德语)的能力,从而导致各个重复的名称类似于:Wiederholung 1 von 5
、Wiederholung 2 von 5
等。
由于 `beforeEach()` 方法使用 `@BeforeEach` 注解,因此它将在每个重复测试的每次重复之前执行。通过将 `TestInfo` 和 `RepetitionInfo` 注入方法,我们看到可以获取有关当前正在执行的重复测试的信息。使用启用了 `INFO` 日志级别的 `RepeatedTestsDemo` 执行将导致以下输出。
INFO: About to execute repetition 1 of 10 for repeatedTest INFO: About to execute repetition 2 of 10 for repeatedTest INFO: About to execute repetition 3 of 10 for repeatedTest INFO: About to execute repetition 4 of 10 for repeatedTest INFO: About to execute repetition 5 of 10 for repeatedTest INFO: About to execute repetition 6 of 10 for repeatedTest INFO: About to execute repetition 7 of 10 for repeatedTest INFO: About to execute repetition 8 of 10 for repeatedTest INFO: About to execute repetition 9 of 10 for repeatedTest INFO: About to execute repetition 10 of 10 for repeatedTest INFO: About to execute repetition 1 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 2 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 3 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 4 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 5 of 5 for repeatedTestWithRepetitionInfo INFO: About to execute repetition 1 of 8 for repeatedTestWithFailureThreshold INFO: About to execute repetition 2 of 8 for repeatedTestWithFailureThreshold INFO: About to execute repetition 3 of 8 for repeatedTestWithFailureThreshold INFO: About to execute repetition 4 of 8 for repeatedTestWithFailureThreshold INFO: About to execute repetition 1 of 1 for customDisplayName INFO: About to execute repetition 1 of 1 for customDisplayNameWithLongPattern INFO: About to execute repetition 1 of 5 for repeatedTestInGerman INFO: About to execute repetition 2 of 5 for repeatedTestInGerman INFO: About to execute repetition 3 of 5 for repeatedTestInGerman INFO: About to execute repetition 4 of 5 for repeatedTestInGerman INFO: About to execute repetition 5 of 5 for repeatedTestInGerman
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.logging.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;
class RepeatedTestsDemo {
private Logger logger = // ...
@BeforeEach
void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();
String methodName = testInfo.getTestMethod().get().getName();
logger.info(String.format("About to execute repetition %d of %d for %s", //
currentRepetition, totalRepetitions, methodName));
}
@RepeatedTest(10)
void repeatedTest() {
// ...
}
@RepeatedTest(5)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
assertEquals(5, repetitionInfo.getTotalRepetitions());
}
@RepeatedTest(value = 8, failureThreshold = 2)
void repeatedTestWithFailureThreshold(RepetitionInfo repetitionInfo) {
// Simulate unexpected failure every second repetition
if (repetitionInfo.getCurrentRepetition() % 2 == 0) {
fail("Boom!");
}
}
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertEquals("Repeat! 1/1", testInfo.getDisplayName());
}
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName());
}
@RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
// ...
}
}
当使用启用了 Unicode 主题的 `ConsoleLauncher` 时,执行 `RepeatedTestsDemo` 将导致以下输出到控制台。
├─ RepeatedTestsDemo ✔ │ ├─ repeatedTest() ✔ │ │ ├─ repetition 1 of 10 ✔ │ │ ├─ repetition 2 of 10 ✔ │ │ ├─ repetition 3 of 10 ✔ │ │ ├─ repetition 4 of 10 ✔ │ │ ├─ repetition 5 of 10 ✔ │ │ ├─ repetition 6 of 10 ✔ │ │ ├─ repetition 7 of 10 ✔ │ │ ├─ repetition 8 of 10 ✔ │ │ ├─ repetition 9 of 10 ✔ │ │ └─ repetition 10 of 10 ✔ │ ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔ │ │ ├─ repetition 1 of 5 ✔ │ │ ├─ repetition 2 of 5 ✔ │ │ ├─ repetition 3 of 5 ✔ │ │ ├─ repetition 4 of 5 ✔ │ │ └─ repetition 5 of 5 ✔ │ ├─ repeatedTestWithFailureThreshold(RepetitionInfo) ✔ │ │ ├─ repetition 1 of 8 ✔ │ │ ├─ repetition 2 of 8 ✘ Boom! │ │ ├─ repetition 3 of 8 ✔ │ │ ├─ repetition 4 of 8 ✘ Boom! │ │ ├─ repetition 5 of 8 ↷ Failure threshold [2] exceeded │ │ ├─ repetition 6 of 8 ↷ Failure threshold [2] exceeded │ │ ├─ repetition 7 of 8 ↷ Failure threshold [2] exceeded │ │ └─ repetition 8 of 8 ↷ Failure threshold [2] exceeded │ ├─ Repeat! ✔ │ │ └─ Repeat! 1/1 ✔ │ ├─ Details... ✔ │ │ └─ Details... :: repetition 1 of 1 ✔ │ └─ repeatedTestInGerman() ✔ │ ├─ Wiederholung 1 von 5 ✔ │ ├─ Wiederholung 2 von 5 ✔ │ ├─ Wiederholung 3 von 5 ✔ │ ├─ Wiederholung 4 von 5 ✔ │ └─ Wiederholung 5 von 5 ✔
2.16. 参数化测试
参数化测试使您可以使用不同的参数多次运行测试。它们的声明方式与普通的 `@Test` 方法相同,但使用 `@ParameterizedTest` 注解代替。此外,您必须声明至少一个源,该源将为每次调用提供参数,然后在测试方法中使用这些参数。
以下示例演示了一个参数化测试,该测试使用 `@ValueSource` 注解来指定 `String` 数组作为参数源。
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
执行上述参数化测试方法时,每次调用都将单独报告。例如,`ConsoleLauncher` 将打印类似于以下内容的输出。
palindromes(String) ✔ ├─ [1] candidate=racecar ✔ ├─ [2] candidate=radar ✔ └─ [3] candidate=able was I ere I saw elba ✔
2.16.1. 必需设置
为了使用参数化测试,您需要添加对 `junit-jupiter-params` 工件的依赖。有关详细信息,请参阅 依赖项元数据。
2.16.2. 使用参数
参数化测试方法通常从配置的源(请参阅 参数源)中直接使用参数,遵循参数源索引和方法参数索引之间的一对一对应关系(请参阅 @CsvSource 中的示例)。但是,参数化测试方法也可以选择将来自源的参数聚合到传递给方法的单个对象中(请参阅 参数聚合)。其他参数也可以由 `ParameterResolver` 提供(例如,获取 `TestInfo`、`TestReporter` 等的实例)。具体来说,参数化测试方法必须根据以下规则声明形式参数。
-
必须首先声明零个或多个索引参数。
-
接下来必须声明零个或多个聚合器。
-
最后必须声明零个或多个由 `ParameterResolver` 提供的参数。
在此上下文中,索引参数是指由 `ArgumentsProvider` 提供的 `Arguments` 中给定索引的参数,该参数作为参数传递给参数化方法,位于方法形式参数列表中的相同索引处。聚合器是任何类型为 `ArgumentsAccessor` 的参数,或任何使用 `@AggregateWith` 注解的参数。
可关闭参数
实现 `java.lang.AutoCloseable`(或扩展 `java.lang.AutoCloseable` 的 `java.io.Closeable`)的参数将在 `@AfterEach` 方法和 `AfterEachCallback` 扩展调用当前参数化测试调用的 `@AfterEach` 方法和 `AfterEachCallback` 扩展后自动关闭。 要阻止这种情况发生,请将 `@ParameterizedTest` 中的 `autoCloseArguments` 属性设置为 `false`。具体来说,如果实现 `AutoCloseable` 的参数被重复用于同一参数化测试方法的多次调用,则必须使用 `@ParameterizedTest(autoCloseArguments = false)` 注解该方法,以确保该参数不会在调用之间关闭。 |
2.16.3. 参数源
开箱即用,JUnit Jupiter 提供了许多源注解。以下每个小节都简要概述了每个注解并提供了一个示例。有关更多信息,请参阅 `org.junit.jupiter.params.provider` 包中的 Javadoc。
@ValueSource
@ValueSource
是最简单的源之一。它允许您指定单个文字值数组,并且只能用于为每次参数化测试调用提供单个参数。
@ValueSource
支持以下类型的文字值。
-
short
-
byte
-
int
-
long
-
float
-
double
-
char
-
boolean
-
java.lang.String
-
java.lang.Class
例如,以下 `@ParameterizedTest` 方法将被调用三次,分别使用值 `1`、`2` 和 `3`。
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
assertTrue(argument > 0 && argument < 4);
}
空源和空源
为了检查极端情况并验证我们的软件在提供错误输入时是否表现正确,将 `null` 和空值提供给我们的参数化测试可能很有用。以下注解用作接受单个参数的参数化测试的 `null` 和空值的源。
-
@NullSource
: 为带注解的 `@ParameterizedTest` 方法提供单个 `null` 参数。-
@NullSource
不能用于具有基本类型参数的参数。
-
-
@EmptySource
: 为带注解的 `@ParameterizedTest` 方法提供单个空参数,用于以下类型的参数:`java.lang.String`、`java.util.Collection`(以及具有 `public` 无参数构造函数的具体子类型)、`java.util.List`、`java.util.Set`、`java.util.SortedSet`、`java.util.NavigableSet`、`java.util.Map`(以及具有 `public` 无参数构造函数的具体子类型)、`java.util.SortedMap`、`java.util.NavigableMap`、基本数组(例如,`int[]`、`char[][]` 等)、对象数组(例如,`String[]`、`Integer[][]` 等)。 -
@NullAndEmptySource
: 一个组合注解,它结合了 `@NullSource` 和 `@EmptySource` 的功能。
如果您需要为参数化测试提供多种类型的空白字符串,可以使用 @ValueSource 来实现这一点,例如,@ValueSource(strings = {" ", " ", "\t", "\n"})
。
您还可以将 `@NullSource`、`@EmptySource` 和 `@ValueSource` 组合在一起,以测试更广泛的 `null`、空和空白输入。以下示例演示了如何为字符串实现这一点。
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
使用组合的 `@NullAndEmptySource` 注解可以简化上述内容,如下所示。
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
`nullEmptyAndBlankStrings(String)` 参数化测试方法的两种变体都将导致六次调用:1 次用于 `null`,1 次用于空字符串,以及 4 次用于通过 `@ValueSource` 提供的显式空白字符串。 |
@EnumSource
@EnumSource
提供了一种方便的方式来使用 Enum
常量。
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
注释的 value
属性是可选的。如果省略,则使用第一个方法参数的声明类型。如果它没有引用枚举类型,则测试将失败。因此,在上面的示例中,value
属性是必需的,因为方法参数被声明为 TemporalUnit
,即 ChronoUnit
实现的接口,它不是枚举类型。将方法参数类型更改为 ChronoUnit
允许您从注释中省略显式枚举类型,如下所示。
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
assertNotNull(unit);
}
该注释提供了一个可选的 names
属性,它允许您指定要使用哪些常量,如以下示例所示。如果省略,将使用所有常量。
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}
@EnumSource
注释还提供了一个可选的 mode
属性,它允许您对传递给测试方法的常量进行细粒度控制。例如,您可以从枚举常量池中排除名称或指定正则表达式,如以下示例所示。
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}
@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
assertTrue(unit.name().endsWith("DAYS"));
}
@MethodSource
@MethodSource
允许您引用测试类或外部类的工厂方法。
测试类中的工厂方法必须是 static
,除非测试类用 @TestInstance(Lifecycle.PER_CLASS)
进行注释;而外部类中的工厂方法必须始终是 static
。
每个工厂方法都必须生成一个参数的流,并且流中的每组参数都将作为带注释的 @ParameterizedTest
方法的单个调用的实际参数提供。一般来说,这转化为一个 Arguments
的 Stream
(即 Stream<Arguments>
);但是,实际的具体返回类型可以采用多种形式。在此上下文中,“流”是指 JUnit 可以可靠地转换为 Stream
的任何东西,例如 Stream
、DoubleStream
、LongStream
、IntStream
、Collection
、Iterator
、Iterable
、对象数组或基本类型数组。“流”中的“参数”可以作为 Arguments
的实例、对象数组(例如 Object[]
)或单个值提供,如果参数化测试方法接受单个参数。
如果您只需要一个参数,您可以返回参数类型的实例的 Stream
,如以下示例所示。
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
如果您没有通过 @MethodSource
显式提供工厂方法名称,JUnit Jupiter 将根据惯例搜索与当前 @ParameterizedTest
方法同名的工厂方法。这在以下示例中得到证明。
@ParameterizedTest
@MethodSource
void testWithDefaultLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> testWithDefaultLocalMethodSource() {
return Stream.of("apple", "banana");
}
基本类型(DoubleStream
、IntStream
和 LongStream
)的流也受支持,如以下示例所示。
@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
assertNotEquals(9, argument);
}
static IntStream range() {
return IntStream.range(0, 20).skip(10);
}
如果参数化测试方法声明多个参数,您需要返回一个 Arguments
实例或对象数组的集合、流或数组,如下所示(有关支持的返回类型的更多详细信息,请参阅 @MethodSource
的 Javadoc)。请注意,arguments(Object…)
是在 Arguments
接口中定义的静态工厂方法。此外,Arguments.of(Object…)
可以用作 arguments(Object…)
的替代方法。
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
可以通过提供其完全限定的方法名来引用外部的 static
工厂方法,如以下示例所示。
package example;
import java.util.stream.Stream;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
class ExternalMethodSourceDemo {
@ParameterizedTest
@MethodSource("example.StringsProviders#tinyStrings")
void testWithExternalMethodSource(String tinyString) {
// test with tiny string
}
}
class StringsProviders {
static Stream<String> tinyStrings() {
return Stream.of(".", "oo", "OOO");
}
}
工厂方法可以声明参数,这些参数将由注册的 ParameterResolver
扩展 API 实现提供。在以下示例中,工厂方法通过其名称引用,因为测试类中只有一个这样的方法。如果有多个具有相同名称的本地方法,也可以提供参数来区分它们 - 例如,@MethodSource("factoryMethod()")
或 @MethodSource("factoryMethod(java.lang.String)")
。或者,可以通过其完全限定的方法名引用工厂方法,例如 @MethodSource("example.MyTests#factoryMethod(java.lang.String)")
。
@RegisterExtension
static final IntegerResolver integerResolver = new IntegerResolver();
@ParameterizedTest
@MethodSource("factoryMethodWithArguments")
void testWithFactoryMethodWithArguments(String argument) {
assertTrue(argument.startsWith("2"));
}
static Stream<Arguments> factoryMethodWithArguments(int quantity) {
return Stream.of(
arguments(quantity + " apples"),
arguments(quantity + " lemons")
);
}
static class IntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == int.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return 2;
}
}
@CsvSource
@CsvSource
允许您将参数列表表示为逗号分隔值(即 CSV String
文字)。通过 @CsvSource
中的 value
属性提供的每个字符串都代表一个 CSV 记录,并导致参数化测试调用一次。第一个记录可以选择用于提供 CSV 标题(有关详细信息和示例,请参阅 useHeadersInDisplayName
属性的 Javadoc)。
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
默认分隔符是逗号 (,
),但您可以通过设置 delimiter
属性来使用另一个字符。或者,delimiterString
属性允许您使用 String
分隔符而不是单个字符。但是,两个分隔符属性不能同时设置。
默认情况下,@CsvSource
使用单引号 ('
) 作为其引号字符,但这可以通过 quoteCharacter
属性更改。请参阅上面示例和下表中的 'lemon, lime'
值。一个空的、带引号的值 (''
) 会导致一个空的 String
,除非设置了 emptyValue
属性;而一个完全空的值将被解释为 null
引用。通过指定一个或多个 nullValues
,可以将自定义值解释为 null
引用(请参阅表中的 NIL
示例)。如果 null
引用的目标类型是基本类型,则会抛出 ArgumentConversionException
。
未加引号的空值将始终转换为 null 引用,无论通过 nullValues 属性配置了哪些自定义值。 |
默认情况下,除了在带引号的字符串中,CSV 列中的前导和尾随空格都会被修剪。可以通过将 ignoreLeadingAndTrailingWhitespace
属性设置为 true
来更改此行为。
示例输入 | 结果参数列表 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
如果您的编程语言支持文本块(例如,Java SE 15 或更高版本),您可以选择使用 @CsvSource
的 textBlock
属性。文本块中的每个记录都代表一个 CSV 记录,并导致参数化测试调用一次。通过将 useHeadersInDisplayName
属性设置为 true
,第一个记录可以选择用于提供 CSV 标题,如以下示例所示。
使用文本块,前面的示例可以实现如下。
@ParameterizedTest(name = "[{index}] {arguments}")
@CsvSource(useHeadersInDisplayName = true, textBlock = """
FRUIT, RANK
apple, 1
banana, 2
'lemon, lime', 0xF1
strawberry, 700_000
""")
void testWithCsvSource(String fruit, int rank) {
// ...
}
为前面的示例生成的显示名称包括 CSV 标题名称。
[1] FRUIT = apple, RANK = 1 [2] FRUIT = banana, RANK = 2 [3] FRUIT = lemon, lime, RANK = 0xF1 [4] FRUIT = strawberry, RANK = 700_000
与通过 value
属性提供的 CSV 记录相反,文本块可以包含注释。任何以 #
符号开头的行都将被视为注释并被忽略。但是请注意,#
符号必须是行上的第一个字符,没有任何前导空格。因此,建议将结束文本块分隔符 ("""
) 放在输入的最后一行末尾或下一行,与输入的其余部分左对齐(如以下示例所示,该示例演示了类似于表格的格式)。
@ParameterizedTest
@CsvSource(delimiter = '|', quoteCharacter = '"', textBlock = """
#-----------------------------
# FRUIT | RANK
#-----------------------------
apple | 1
#-----------------------------
banana | 2
#-----------------------------
"lemon lime" | 0xF1
#-----------------------------
strawberry | 700_000
#-----------------------------
""")
void testWithCsvSource(String fruit, int rank) {
// ...
}
Java 的 文本块 功能在代码编译时会自动删除偶然的空格。但是,其他 JVM 语言(如 Groovy 和 Kotlin)则不会。因此,如果您使用的是 Java 以外的编程语言,并且您的文本块在带引号的字符串中包含注释或换行符,则需要确保文本块中没有前导空格。 |
@CsvFileSource
@CsvFileSource
允许您从类路径或本地文件系统中使用逗号分隔值 (CSV) 文件。来自 CSV 文件的每个记录都会导致参数化测试调用一次。第一个记录可以选择用于提供 CSV 标题。您可以指示 JUnit 通过 numLinesToSkip
属性忽略标题。如果您希望在显示名称中使用标题,可以将 useHeadersInDisplayName
属性设置为 true
。以下示例演示了 numLinesToSkip
和 useHeadersInDisplayName
的用法。
默认分隔符是逗号 (,
),但您可以通过设置 delimiter
属性来使用另一个字符。或者,delimiterString
属性允许您使用 String
分隔符而不是单个字符。但是,两个分隔符属性不能同时设置。
CSV 文件中的注释 任何以 # 符号开头的行都将被解释为注释,并将被忽略。 |
@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromClasspath(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
@ParameterizedTest
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
@ParameterizedTest(name = "[{index}] {arguments}")
@CsvFileSource(resources = "/two-column.csv", useHeadersInDisplayName = true)
void testWithCsvFileSourceAndHeaders(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
COUNTRY, REFERENCE
Sweden, 1
Poland, 2
"United States of America", 3
France, 700_000
以下列表显示了上面前两个参数化测试方法的生成的显示名称。
[1] country=Sweden, reference=1 [2] country=Poland, reference=2 [3] country=United States of America, reference=3 [4] country=France, reference=700_000
以下列表显示了上面最后一个使用 CSV 标题名称的参数化测试方法的生成的显示名称。
[1] COUNTRY = Sweden, REFERENCE = 1 [2] COUNTRY = Poland, REFERENCE = 2 [3] COUNTRY = United States of America, REFERENCE = 3 [4] COUNTRY = France, REFERENCE = 700_000
与 @CsvSource
中使用的默认语法相反,@CsvFileSource
默认情况下使用双引号 ("
) 作为引号字符,但这可以通过 quoteCharacter
属性更改。请参阅上面示例中的 "United States of America"
值。一个空的、带引号的值 (""
) 会导致一个空的 String
,除非设置了 emptyValue
属性;而一个完全空的值将被解释为 null
引用。通过指定一个或多个 nullValues
,可以将自定义值解释为 null
引用。如果 null
引用的目标类型是基本类型,则会抛出 ArgumentConversionException
。
未加引号的空值将始终转换为 null 引用,无论通过 nullValues 属性配置了哪些自定义值。 |
默认情况下,除了在带引号的字符串中,CSV 列中的前导和尾随空格都会被修剪。可以通过将 ignoreLeadingAndTrailingWhitespace
属性设置为 true
来更改此行为。
@ArgumentsSource
@ArgumentsSource
可用于指定自定义的可重用 ArgumentsProvider
。请注意,ArgumentsProvider
的实现必须声明为顶级类或 static
嵌套类。
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}
如果您希望实现一个也使用注释的自定义 ArgumentsProvider
(如内置提供程序,例如 ValueArgumentsProvider
或 CsvArgumentsProvider
),您可以选择扩展 AnnotationBasedArgumentsProvider
类。
2.16.4. 参数转换
扩展转换
JUnit Jupiter 支持 扩展基本类型转换,用于提供给 @ParameterizedTest
的参数。例如,用 @ValueSource(ints = { 1, 2, 3 })
注释的参数化测试可以声明为不仅接受 int
类型的参数,还接受 long
、float
或 double
类型的参数。
隐式转换
为了支持像 @CsvSource
这样的用例,JUnit Jupiter 提供了一些内置的隐式类型转换器。转换过程取决于每个方法参数的声明类型。
例如,如果一个 @ParameterizedTest
声明了一个类型为 TimeUnit
的参数,而声明的源提供的实际类型是一个 String
,那么该字符串将自动转换为相应的 TimeUnit
枚举常量。
@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(ChronoUnit argument) {
assertNotNull(argument.name());
}
String
实例会隐式转换为以下目标类型。
十进制、十六进制和八进制 String 字面量将被转换为它们的整型类型:byte 、short 、int 、long 以及它们的包装类型。 |
目标类型 | 示例 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
回退字符串到对象转换
除了上面表格中列出的从字符串到目标类型的隐式转换之外,JUnit Jupiter 还提供了一种回退机制,用于在目标类型声明了一个适合的工厂方法或工厂构造函数时,从String
到给定目标类型的自动转换,如下定义。
-
工厂方法:在目标类型中声明的非私有、
static
方法,它接受一个String
参数并返回目标类型的实例。方法的名称可以是任意的,不需要遵循任何特定的约定。 -
工厂构造函数:目标类型中接受一个
String
参数的非私有构造函数。请注意,目标类型必须声明为顶级类或static
嵌套类。
如果发现多个工厂方法,它们将被忽略。如果发现一个工厂方法和一个工厂构造函数,将使用工厂方法而不是构造函数。 |
例如,在下面的@ParameterizedTest
方法中,Book
参数将通过调用Book.fromTitle(String)
工厂方法并传递"42 Cats"
作为书籍的标题来创建。
@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
assertEquals("42 Cats", book.getTitle());
}
public class Book {
private final String title;
private Book(String title) {
this.title = title;
}
public static Book fromTitle(String title) {
return new Book(title);
}
public String getTitle() {
return this.title;
}
}
显式转换
您可以通过使用@ConvertWith
注解为特定参数显式指定要使用的ArgumentConverter
,而不是依赖于隐式参数转换,如下面的示例所示。请注意,ArgumentConverter
的实现必须声明为顶级类或static
嵌套类。
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(ChronoUnit.valueOf(argument));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
assertEquals(String.class, targetType, "Can only convert to String");
if (source instanceof Enum<?>) {
return ((Enum<?>) source).name();
}
return String.valueOf(source);
}
}
如果转换器只用于将一种类型转换为另一种类型,您可以扩展TypedArgumentConverter
以避免样板类型检查。
public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {
protected ToLengthArgumentConverter() {
super(String.class, Integer.class);
}
@Override
protected Integer convert(String source) {
return (source != null ? source.length() : 0);
}
}
显式参数转换器旨在由测试和扩展作者实现。因此,junit-jupiter-params
只提供了一个显式参数转换器,它也可以作为参考实现:JavaTimeArgumentConverter
。它通过组合注解JavaTimeConversionPattern
使用。
@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(
@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
assertEquals(2017, argument.getYear());
}
如果您希望实现一个也使用注解的自定义ArgumentConverter
(如JavaTimeArgumentConverter
),您可以扩展AnnotationBasedArgumentConverter
类。
2.16.5. 参数聚合
默认情况下,提供给@ParameterizedTest
方法的每个参数对应于一个方法参数。因此,预期提供大量参数的参数源会导致方法签名过长。
在这种情况下,可以使用ArgumentsAccessor
而不是多个参数。使用此 API,您可以通过传递给测试方法的单个参数访问提供的参数。此外,如隐式转换中所述,支持类型转换。
此外,您可以使用ArgumentsAccessor.getInvocationIndex()
检索当前测试调用索引。
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
Person person = new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, Gender.class),
arguments.get(3, LocalDate.class));
if (person.getFirstName().equals("Jane")) {
assertEquals(Gender.F, person.getGender());
}
else {
assertEquals(Gender.M, person.getGender());
}
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
ArgumentsAccessor
的实例会自动注入到任何类型为ArgumentsAccessor
的参数中。
自定义聚合器
除了使用ArgumentsAccessor
直接访问@ParameterizedTest
方法的参数之外,JUnit Jupiter 还支持使用自定义的可重用聚合器。
要使用自定义聚合器,请实现ArgumentsAggregator
接口,并通过@ParameterizedTest
方法中兼容参数上的@AggregateWith
注解注册它。然后,在调用参数化测试时,聚合的结果将作为对应参数的参数提供。请注意,ArgumentsAggregator
的实现必须声明为顶级类或static
嵌套类。
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
// perform assertions against person
}
public class PersonAggregator implements ArgumentsAggregator {
@Override
public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) {
return new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, Gender.class),
arguments.get(3, LocalDate.class));
}
}
如果您发现自己在代码库中为多个参数化测试方法重复声明@AggregateWith(MyTypeAggregator.class)
,您可能希望创建一个自定义的组合注解,例如@CsvToMyType
,它使用@AggregateWith(MyTypeAggregator.class)
进行元注解。以下示例演示了使用自定义@CsvToPerson
注解的实际操作。
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
// perform assertions against person
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}
2.16.6. 自定义显示名称
默认情况下,参数化测试调用的显示名称包含调用索引和该特定调用所有参数的String
表示形式。每个参数前面都带有参数名称(除非参数仅通过ArgumentsAccessor
或ArgumentAggregator
可用),如果存在于字节码中(对于 Java,测试代码必须使用-parameters
编译器标志进行编译)。
但是,您可以通过@ParameterizedTest
注解的name
属性自定义调用显示名称,如下面的示例所示。
@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
void testWithCustomDisplayNames(String fruit, int rank) {
}
当使用ConsoleLauncher
执行上述方法时,您将看到类似于以下内容的输出。
Display name of container ✔ ├─ 1 ==> the rank of 'apple' is 1 ✔ ├─ 2 ==> the rank of 'banana' is 2 ✔ └─ 3 ==> the rank of 'lemon, lime' is 3 ✔
请注意,name
是一个MessageFormat
模式。因此,单引号('
)需要表示为双单引号(''
)才能显示。
以下占位符在自定义显示名称中受支持。
占位符 | 描述 |
---|---|
|
方法的显示名称 |
|
当前调用索引(从 1 开始) |
|
完整的、用逗号分隔的参数列表 |
|
完整的、用逗号分隔的参数列表,带有参数名称 |
|
单个参数 |
当在显示名称中包含参数时,如果它们的字符串表示形式超过配置的最大长度,则会将其截断。该限制可以通过junit.jupiter.params.displayname.argument.maxlength 配置参数进行配置,默认值为 512 个字符。 |
当使用@MethodSource
或@ArgumentsSource
时,您可以使用Named
API 为参数提供自定义名称。如果参数包含在调用显示名称中,则将使用自定义名称,如下面的示例所示。
@DisplayName("A parameterized test with named arguments")
@ParameterizedTest(name = "{index}: {0}")
@MethodSource("namedArguments")
void testWithNamedArguments(File file) {
}
static Stream<Arguments> namedArguments() {
return Stream.of(
arguments(named("An important file", new File("path1"))),
arguments(named("Another file", new File("path2")))
);
}
A parameterized test with named arguments ✔ ├─ 1: An important file ✔ └─ 2: Another file ✔
如果您希望为项目中的所有参数化测试设置默认名称模式,您可以在junit-platform.properties
文件中声明junit.jupiter.params.displayname.default
配置参数,如下面的示例所示(有关其他选项,请参阅配置参数)。
junit.jupiter.params.displayname.default = {index}
参数化测试的显示名称是根据以下优先级规则确定的
-
@ParameterizedTest
中的name
属性,如果存在 -
junit.jupiter.params.displayname.default
配置参数的值,如果存在 -
@ParameterizedTest
中定义的DEFAULT_DISPLAY_NAME
常量
2.16.7. 生命周期和互操作性
参数化测试的每次调用都与常规的@Test
方法具有相同的生命周期。例如,@BeforeEach
方法将在每次调用之前执行。与动态测试类似,调用将在 IDE 的测试树中逐个出现。您可以在同一个测试类中随意混合常规的@Test
方法和@ParameterizedTest
方法。
您可以将ParameterResolver
扩展与@ParameterizedTest
方法一起使用。但是,由参数源解析的方法参数需要在参数列表中排在首位。由于测试类可能包含常规测试以及参数列表不同的参数化测试,因此不会为生命周期方法(例如@BeforeEach
)和测试类构造函数解析参数源中的值。
@BeforeEach
void beforeEach(TestInfo testInfo) {
// ...
}
@ParameterizedTest
@ValueSource(strings = "apple")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
testReporter.publishEntry("argument", argument);
}
@AfterEach
void afterEach(TestInfo testInfo) {
// ...
}
2.17. 测试模板
一个@TestTemplate
方法不是一个普通的测试用例,而是一个测试用例的模板。因此,它被设计为根据注册的提供者返回的调用上下文数量多次调用。因此,它必须与注册的TestTemplateInvocationContextProvider
扩展一起使用。每次调用测试模板方法的行为都类似于执行一个普通的@Test
方法,完全支持相同的生命周期回调和扩展。有关使用示例,请参阅为测试模板提供调用上下文。
2.18. 动态测试
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
方法不能是private
或static
,并且可以选择声明由ParameterResolvers
解析的参数。
DynamicTest
是在运行时生成的测试用例。它由显示名称和Executable
组成。Executable
是一个@FunctionalInterface
,这意味着动态测试的实现可以作为lambda 表达式或方法引用提供。
动态测试生命周期 动态测试的执行生命周期与标准@Test 用例的执行生命周期大不相同。具体来说,没有针对单个动态测试的生命周期回调。这意味着@BeforeEach 和@AfterEach 方法及其对应的扩展回调将针对@TestFactory 方法执行,但不会针对每个动态测试执行。换句话说,如果您在动态测试的 lambda 表达式中访问测试实例中的字段,那么这些字段不会在由同一个@TestFactory 方法生成的单个动态测试执行之间被回调方法或扩展重置。 |
从 JUnit Jupiter 5.10.2 开始,动态测试必须始终由工厂方法创建;但是,这可能会在以后的版本中通过注册机制得到补充。
2.18.1. 动态测试示例
以下DynamicTestsDemo
类演示了测试工厂和动态测试的几个示例。
第一个方法返回无效的返回类型。由于在编译时无法检测到无效的返回类型,因此在运行时检测到它时会抛出JUnitException
。
接下来的六个方法演示了生成Collection
、Iterable
、Iterator
、数组或Stream
的DynamicTest
实例。这些示例中的大多数实际上并没有表现出动态行为,而只是原则上演示了支持的返回类型。但是,dynamicTestsFromStream()
和dynamicTestsFromIntStream()
演示了如何为给定的一组字符串或一系列输入数字生成动态测试。
下一个方法本质上是动态的。generateRandomNumberOfTests()
实现了一个Iterator
,它生成随机数、显示名称生成器和测试执行器,然后将所有三个提供给DynamicTest.stream()
。虽然generateRandomNumberOfTests()
的非确定性行为当然与测试可重复性相冲突,因此应该谨慎使用,但它用于演示动态测试的表达能力和强大功能。
下一个方法在灵活性方面类似于generateRandomNumberOfTests()
;但是,dynamicTestsFromStreamFactoryMethod()
通过DynamicTest.stream()
工厂方法从现有的Stream
生成动态测试流。
为了演示目的,dynamicNodeSingleTest()
方法生成单个DynamicTest
而不是流,而dynamicNodeSingleContainer()
方法使用DynamicContainer
生成动态测试的嵌套层次结构。
import static example.util.StringUtils.isPalindrome;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.junit.jupiter.api.Named.named;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import example.util.Calculator;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
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", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(
dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
).iterator();
}
@TestFactory
DynamicTest[] dynamicTestsFromArray() {
return new DynamicTest[] {
dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
};
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
@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, () -> assertTrue(n % 2 == 0)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {
// 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) -> 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");
// Generates 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(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(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(isPalindrome("pop")));
}
@TestFactory
DynamicNode dynamicNodeSingleContainer() {
return dynamicContainer("palindromes",
Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
));
}
}
2.18.2. 动态测试的 URI 测试源
JUnit Platform 提供了TestSource
,它表示测试或容器的来源,用于通过 IDE 和构建工具导航到其位置。
动态测试或动态容器的TestSource
可以从java.net.URI
构建,该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
包含method
方案和完全限定的方法名称 (FQMN),例如method:org.junit.Foo#bar(java.lang.String, java.lang.String[])
。有关 FQMN 支持的格式,请参阅DiscoverySelectors.selectMethod(String)
的 Javadoc。 ClassSource
-
如果
URI
包含class
方案和完全限定的类名,例如class:org.junit.Foo?line=42
。 UriSource
-
如果上述
TestSource
实现都不适用。
2.19. 超时
@Timeout
注解允许声明测试、测试工厂、测试模板或生命周期方法在执行时间超过给定持续时间时应该失败。持续时间的单位默认为秒,但可以配置。
以下示例展示了如何将@Timeout
应用于生命周期和测试方法。
class TimeoutDemo {
@BeforeEach
@Timeout(5)
void setUp() {
// fails if execution time exceeds 5 seconds
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds500Milliseconds() {
// fails if execution time exceeds 500 milliseconds
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS, threadMode = ThreadMode.SEPARATE_THREAD)
void failsIfExecutionTimeExceeds500MillisecondsInSeparateThread() {
// fails if execution time exceeds 500 milliseconds, the test code is executed in a separate thread
}
}
要将相同的超时应用于测试类及其所有@Nested
类中的所有测试方法,可以在类级别声明@Timeout
注解。然后,它将应用于该类及其@Nested
类中的所有测试、测试工厂和测试模板方法,除非被特定方法或@Nested
类上的@Timeout
注解覆盖。请注意,在类级别声明的@Timeout
注解不应用于生命周期方法。
在@TestFactory
方法上声明@Timeout
会检查工厂方法是否在指定持续时间内返回,但不会验证工厂生成的每个单独的DynamicTest
的执行时间。为此,请使用assertTimeout()
或assertTimeoutPreemptively()
。
如果@Timeout
存在于@TestTemplate
方法上,例如@RepeatedTest
或@ParameterizedTest
,则每次调用都会应用给定的超时。
2.19.1. 线程模式
超时可以使用以下三种线程模式之一应用:SAME_THREAD
、SEPARATE_THREAD
或INFERRED
。
当使用SAME_THREAD
时,注解方法的执行在测试的主线程中进行。如果超时,主线程将从另一个线程中断。这样做是为了确保与 Spring 等框架的互操作性,这些框架使用对当前运行线程敏感的机制,例如ThreadLocal
事务管理。
相反,当使用SEPARATE_THREAD
时,就像assertTimeoutPreemptively()
断言一样,注解方法的执行在单独的线程中进行,这会导致不良副作用,请参阅使用assertTimeoutPreemptively()
的抢占式超时。
当使用INFERRED
(默认)线程模式时,线程模式通过junit.jupiter.execution.timeout.thread.mode.default
配置参数解析。如果提供的配置参数无效或不存在,则使用SAME_THREAD
作为回退。
2.19.2. 默认超时
以下配置参数可用于指定所有类别方法的默认超时,除非它们或封闭的测试类用@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.testable.method.default
会覆盖junit.jupiter.execution.timeout.default
。
此类配置参数的值必须采用以下不区分大小写的格式:<number> [ns|μs|ms|s|m|h|d]
。数字和单位之间的空格可以省略。不指定单位等效于使用秒。
参数值 | 等效注解 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.19.3. 使用 @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
}
如果您需要对轮询间隔进行更多控制,并对异步测试有更大的灵活性,请考虑使用专门的库,例如 Awaitility。 |
2.20. 并行执行
默认情况下,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
模式或 MethodOrderer
(除了 MethodOrderer.Random
)的测试类。在前一种情况下,测试作者必须确保测试类是线程安全的;在后一种情况下,并发执行可能会与配置的执行顺序冲突。因此,在这两种情况下,只有在测试类或方法上存在 @Execution(CONCURRENT)
注解时,此类测试类中的测试方法才会并发执行。
当启用并行执行并注册默认 ClassOrderer
时(有关详细信息,请参阅 类顺序),顶级测试类将首先按顺序排序并按该顺序调度。但是,不能保证它们会完全按该顺序启动,因为执行它们的线程不受 JUnit 的直接控制。
此外,您可以通过设置 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
下图说明了两个顶级测试类 A
和 B
(每个类有两个测试方法)的执行方式,针对 junit.jupiter.execution.parallel.mode.default
和 junit.jupiter.execution.parallel.mode.classes.default
的所有四种组合(请参阅第一列中的标签)。
如果未显式设置 junit.jupiter.execution.parallel.mode.classes.default
配置参数,则将使用 junit.jupiter.execution.parallel.mode.default
的值。
2.20.1. 配置
可以使用 ParallelExecutionConfigurationStrategy
配置属性,例如所需的并行度和最大池大小。JUnit Platform 提供了两种开箱即用的实现:dynamic
和 fixed
。或者,您可以实现一个 custom
策略。
要选择策略,请将 junit.jupiter.execution.parallel.config.strategy
配置参数设置为以下选项之一。
dynamic
-
根据可用处理器/内核数量乘以
junit.jupiter.execution.parallel.config.dynamic.factor
配置参数(默认为1
)来计算所需的并行度。可选的junit.jupiter.execution.parallel.config.dynamic.max-pool-size-factor
配置参数可用于限制最大线程数。 fixed
-
使用强制的
junit.jupiter.execution.parallel.config.fixed.parallelism
配置参数作为所需的并行度。可选的junit.jupiter.execution.parallel.config.fixed.max-pool-size
配置参数可用于限制最大线程数。 custom
-
允许您通过强制的
junit.jupiter.execution.parallel.config.custom.class
配置参数指定自定义ParallelExecutionConfigurationStrategy
实现,以确定所需的配置。
如果未设置任何配置策略,JUnit Jupiter 将使用 dynamic
配置策略,其因子为 1
。因此,所需的并行度将等于可用处理器/内核的数量。
并行度本身并不意味着最大并发线程数 默认情况下,JUnit Jupiter 不保证并发执行的测试数量不会超过配置的并行度。例如,当使用下一节中描述的同步机制之一时,幕后使用的 ForkJoinPool 可能会生成额外的线程以确保执行以足够的并行度继续。如果您需要此类保证,使用 Java 9+,可以通过控制 dynamic 、fixed 和 custom 策略的最大池大小来限制最大并发线程数。 |
相关属性
下表列出了用于配置并行执行的相关属性。有关如何设置此类属性的详细信息,请参阅 配置参数。
属性 | 描述 | 支持的值 | 默认值 |
---|---|---|---|
|
启用并行测试执行 |
|
|
|
测试树中节点的默认执行模式 |
|
|
|
顶级类的默认执行模式 |
|
|
|
用于所需并行度和最大池大小的执行策略 |
|
|
|
用于确定 |
正小数 |
|
|
用于确定 |
正小数,必须大于或等于 |
256 + |
|
禁用 |
|
|
|
|
正整数 |
无默认值 |
|
|
正整数,必须大于或等于 |
256 + |
|
禁用 |
|
|
|
要用于 |
例如,org.example.CustomStrategy |
无默认值 |
2.20.2. 同步
除了使用@Execution
注解控制执行模式之外,JUnit Jupiter还提供另一种基于注解的声明式同步机制。@ResourceLock
注解允许您声明测试类或方法使用特定共享资源,该资源需要同步访问以确保可靠的测试执行。共享资源由一个唯一的名称标识,该名称是一个String
。该名称可以是用户定义的,也可以是Resources
中的预定义常量之一:SYSTEM_PROPERTIES
、SYSTEM_OUT
、SYSTEM_ERR
、LOCALE
或TIME_ZONE
。
如果以下示例中的测试在没有使用@ResourceLock
的情况下并行运行,它们将是不稳定的。有时它们会通过,有时它们会由于写入和读取同一个 JVM 系统属性的固有竞争条件而失败。
当使用@ResourceLock
注解声明对共享资源的访问时,JUnit Jupiter 引擎将使用此信息来确保没有冲突的测试并行运行。
隔离运行测试
如果您的大多数测试类可以在没有任何同步的情况下并行运行,但您有一些测试类需要隔离运行,您可以使用 |
除了唯一标识共享资源的String
之外,您还可以指定访问模式。两个需要对共享资源进行READ
访问的测试可以彼此并行运行,但不能在任何其他需要对同一个共享资源进行READ_WRITE
访问的测试运行时并行运行。
@Execution(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 = 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"));
}
}
2.21. 内置扩展
虽然 JUnit 团队鼓励将可重用扩展打包并在单独的库中维护,但 JUnit Jupiter API 工件包含一些面向用户的扩展实现,这些实现被认为是如此普遍有用,以至于用户不必添加另一个依赖项。
2.21.1. TempDirectory 扩展
内置的TempDirectory
扩展用于为单个测试或测试类中的所有测试创建和清理临时目录。它默认注册。要使用它,请使用@TempDir
注解类型为java.nio.file.Path
或java.io.File
的非 final、未分配字段,或向生命周期方法或测试方法添加类型为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));
}
要恢复使用单个临时目录(根据注解使用级别,用于整个测试类或方法)的旧行为,您可以将junit.jupiter.tempdir.scope 配置参数设置为per_context 。但是,请注意,此选项已弃用,将在将来的版本中删除。 |
@TempDir
不支持构造函数参数。如果您希望在生命周期方法和当前测试方法中保留对临时目录的单个引用,请通过使用@TempDir
注解实例字段来使用字段注入。
以下示例将共享临时目录存储在static
字段中。这允许在测试类的所有生命周期方法和测试方法中使用相同的sharedTempDir
。为了更好地隔离,您应该使用实例字段,以便每个测试方法使用单独的目录。
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
}
}
@TempDir
注解有一个可选的cleanup
属性,可以设置为NEVER
、ON_SUCCESS
或ALWAYS
。如果清理模式设置为NEVER
,则在测试完成后不会删除临时目录。如果设置为ON_SUCCESS
,则仅在测试成功完成后才会删除临时目录。
默认清理模式为ALWAYS
。您可以使用junit.jupiter.tempdir.cleanup.mode.default
配置参数来覆盖此默认值。
class CleanupModeDemo {
@Test
void fileTest(@TempDir(cleanup = ON_SUCCESS) Path tempDir) {
// perform test
}
}
@TempDir
支持通过可选的factory
属性以编程方式创建临时目录。这通常用于控制临时目录的创建,例如定义父目录或应使用的文件系统。
工厂可以通过实现TempDirFactory
来创建。实现必须提供一个无参数构造函数,并且不应对何时以及如何多次实例化它们做出任何假设,但它们可以假设它们的createTempDirectory(…)
和close()
方法都将被调用一次,按此顺序,并且来自同一个线程。
Jupiter 中可用的默认实现将目录创建委托给java.nio.file.Files::createTempDirectory
,并将junit
作为前缀字符串传递,用于生成目录的名称。
以下示例定义了一个工厂,该工厂使用测试名称作为目录名称前缀,而不是junit
常量值。
class TempDirFactoryDemo {
@Test
void factoryTest(@TempDir(factory = Factory.class) Path tempDir) {
assertTrue(tempDir.getFileName().toString().startsWith("factoryTest"));
}
static class Factory implements TempDirFactory {
@Override
public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
throws IOException {
return Files.createTempDirectory(extensionContext.getRequiredTestMethod().getName());
}
}
}
也可以使用内存文件系统(如Jimfs
)来创建临时目录。以下示例演示了如何实现这一点。
class InMemoryTempDirDemo {
@Test
void test(@TempDir(factory = JimfsTempDirFactory.class) Path tempDir) {
// perform test
}
static class JimfsTempDirFactory implements TempDirFactory {
private final FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());
@Override
public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
throws IOException {
return Files.createTempDirectory(fileSystem.getPath("/"), "junit");
}
@Override
public void close() throws IOException {
fileSystem.close();
}
}
}
@TempDir
也可以用作元注解以减少重复。以下代码清单显示了如何创建自定义的@JimfsTempDir
注解,该注解可以用作@TempDir(factory = JimfsTempDirFactory.class)
的直接替换。
@TempDir
元注解的自定义注解@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@TempDir(factory = JimfsTempDirFactory.class)
@interface JimfsTempDir {
}
以下示例演示了如何使用自定义的@JimfsTempDir
注解。
class JimfsTempDirAnnotationDemo {
@Test
void test(@JimfsTempDir Path tempDir) {
// perform test
}
}
TempDir
注解声明的字段或参数上的元注解或附加注解可能会公开其他属性来配置工厂。这些注解和相关属性可以通过createTempDirectory
的AnnotatedElementContext
参数访问。
您可以使用junit.jupiter.tempdir.factory.default
配置参数来指定您希望默认使用的TempDirFactory
的完全限定类名。与通过@TempDir
注解的factory
属性配置的工厂一样,提供的类必须实现TempDirFactory
接口。默认工厂将用于所有@TempDir
注解,除非注解的factory
属性指定了不同的工厂。
总之,临时目录的工厂是根据以下优先级规则确定的
-
如果存在,则为
@TempDir
注解的factory
属性 -
如果存在,则为通过配置参数配置的默认
TempDirFactory
-
否则,将使用
org.junit.jupiter.api.io.TempDirFactory$Standard
。
3. 从 JUnit 4 迁移
虽然 JUnit Jupiter 编程模型和扩展模型不支持 JUnit 4 功能(如Rules
和Runners
),但预计源代码维护人员无需更新其所有现有测试、测试扩展和自定义构建测试基础设施来迁移到 JUnit Jupiter。
相反,JUnit 通过JUnit Vintage 测试引擎提供了一个平滑的迁移路径,该引擎允许基于 JUnit 3 和 JUnit 4 的现有测试使用 JUnit Platform 基础设施执行。由于所有特定于 JUnit Jupiter 的类和注解都位于org.junit.jupiter
基本包下,因此在类路径中同时存在 JUnit 4 和 JUnit Jupiter 不会导致任何冲突。因此,可以安全地将现有的 JUnit 4 测试与 JUnit Jupiter 测试一起维护。此外,由于 JUnit 团队将继续为 JUnit 4.x 基线提供维护和错误修复版本,因此开发人员有充足的时间根据自己的时间表迁移到 JUnit Jupiter。
3.1. 在 JUnit Platform 上运行 JUnit 4 测试
确保junit-vintage-engine
工件位于您的测试运行时路径中。在这种情况下,JUnit 3 和 JUnit 4 测试将自动被 JUnit Platform 启动器拾取。
请参阅junit5-samples
存储库中的示例项目,了解如何使用 Gradle 和 Maven 完成此操作。
3.2. 迁移提示
以下是在将现有的 JUnit 4 测试迁移到 JUnit Jupiter 时应注意的主题。
-
注解位于
org.junit.jupiter.api
包中。 -
断言位于
org.junit.jupiter.api.Assertions
中。 -
假设位于
org.junit.jupiter.api.Assumptions
中。-
请注意,JUnit Jupiter 5.4 及更高版本支持来自 JUnit 4 的
org.junit.Assume
类的假设方法。具体来说,JUnit Jupiter 支持 JUnit 4 的AssumptionViolatedException
来表示应中止测试,而不是将其标记为失败。
-
-
@Before
和@After
不再存在;请改用@BeforeEach
和@AfterEach
。 -
@BeforeClass
和@AfterClass
不再存在;请改用@BeforeAll
和@AfterAll
。 -
@Ignore
不再存在:请改用@Disabled
或其他内置的执行条件-
另请参阅JUnit 4 @Ignore 支持。
-
-
@Category
不再存在;请改用@Tag
。 -
@RunWith
不再存在;被@ExtendWith
取代。 -
@Rule
和@ClassRule
不再存在;被@ExtendWith
和@RegisterExtension
取代。-
另请参阅有限的 JUnit 4 规则支持。
-
-
@Test(expected = …)
和ExpectedException
规则不再存在;请改用Assertions.assertThrows(…)
。-
如果您仍然需要使用
ExpectedException
,请参阅有限的 JUnit 4 规则支持。
-
-
JUnit Jupiter 中的断言和假设接受失败消息作为其最后一个参数,而不是第一个参数。
-
有关详细信息,请参阅失败消息参数。
-
3.3. 有限的 JUnit 4 规则支持
如上所述,JUnit Jupiter 本身不支持也不打算支持 JUnit 4 的规则。然而,JUnit 团队意识到,许多组织,尤其是大型组织,可能拥有大量使用自定义规则的 JUnit 4 代码库。为了服务这些组织并提供逐步迁移路径,JUnit 团队决定在 JUnit Jupiter 中直接支持部分 JUnit 4 规则。这种支持基于适配器,仅限于语义上与 JUnit Jupiter 扩展模型兼容的规则,即那些不会完全改变测试整体执行流程的规则。
JUnit Jupiter 中的 junit-jupiter-migrationsupport
模块目前支持以下三种 Rule
类型,包括这些类型的子类
-
org.junit.rules.ExternalResource
(包括org.junit.rules.TemporaryFolder
) -
org.junit.rules.Verifier
(包括org.junit.rules.ErrorCollector
) -
org.junit.rules.ExpectedException
与 JUnit 4 中一样,支持使用 Rule
注解的字段和方法。通过在测试类上使用这些类级别扩展,可以保持遗留代码库中的 Rule
实现不变,包括 JUnit 4 规则导入语句。
这种有限的 Rule
支持可以通过类级别注解 @EnableRuleMigrationSupport
开启。此注解是一个组合注解,它启用所有规则迁移支持扩展:VerifierSupport
、ExternalResourceSupport
和 ExpectedExceptionSupport
。您也可以选择使用 @EnableJUnit4MigrationSupport
注解您的测试类,它会注册规则的迁移支持以及 JUnit 4 的 @Ignore
注解(参见 JUnit 4 @Ignore 支持)。
但是,如果您打算为 JUnit Jupiter 开发新的扩展,请使用 JUnit Jupiter 的新扩展模型,而不是 JUnit 4 的基于规则的模型。
3.4. JUnit 4 @Ignore 支持
为了提供从 JUnit 4 到 JUnit Jupiter 的平滑迁移路径,junit-jupiter-migrationsupport
模块提供了对 JUnit 4 的 @Ignore
注解的支持,类似于 Jupiter 的 @Disabled
注解。
要在基于 JUnit Jupiter 的测试中使用 @Ignore
,请在您的构建中配置对 junit-jupiter-migrationsupport
模块的测试依赖,然后使用 @ExtendWith(IgnoreCondition.class)
或 @EnableJUnit4MigrationSupport
(它会自动注册 IgnoreCondition
以及 有限的 JUnit 4 规则支持)注解您的测试类。IgnoreCondition
是一个 ExecutionCondition
,它会禁用使用 @Ignore
注解的测试类或测试方法。
import org.junit.Ignore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.migrationsupport.EnableJUnit4MigrationSupport;
// @ExtendWith(IgnoreCondition.class)
@EnableJUnit4MigrationSupport
class IgnoredTestsDemo {
@Ignore
@Test
void testWillBeIgnored() {
}
@Test
void testWillBeExecuted() {
}
}
3.5. 错误消息参数
JUnit Jupiter 中的 Assumptions
和 Assertions
类声明参数的顺序与 JUnit 4 中不同。在 JUnit 4 中,断言和假设方法接受错误消息作为第一个参数;而在 JUnit Jupiter 中,断言和假设方法接受错误消息作为最后一个参数。
例如,JUnit 4 中的 assertEquals
方法声明为 assertEquals(String message, Object expected, Object actual)
,但在 JUnit Jupiter 中,它声明为 assertEquals(Object expected, Object actual, String message)
。这样做的原因是错误消息是可选的,可选参数应该在方法签名中声明在必需参数之后。
受此更改影响的方法如下
-
断言
-
assertTrue
-
assertFalse
-
assertNull
-
assertNotNull
-
assertEquals
-
assertNotEquals
-
assertArrayEquals
-
assertSame
-
assertNotSame
-
assertThrows
-
-
假设
-
assumeTrue
-
assumeFalse
-
4. 运行测试
4.1. IDE 支持
4.1.1. IntelliJ IDEA
IntelliJ IDEA 从 2016.2 版本开始支持在 JUnit Platform 上运行测试。有关更多信息,请参阅此 IntelliJ IDEA 资源。但是,建议使用 IDEA 2017.3 或更高版本,因为 IDEA 的较新版本会根据项目中使用的 API 版本自动下载以下 JAR 文件:junit-platform-launcher
、junit-jupiter-engine
和 junit-vintage-engine
。
IntelliJ IDEA 2017.3 之前的版本捆绑了特定版本的 JUnit 5。因此,如果您想使用更新版本的 JUnit Jupiter,IDE 中的测试执行可能会因版本冲突而失败。在这种情况下,请按照以下说明使用比 IntelliJ IDEA 捆绑的版本更新的 JUnit 5 版本。 |
为了使用不同的 JUnit 5 版本(例如,5.10.2),您可能需要将相应的 junit-platform-launcher
、junit-jupiter-engine
和 junit-vintage-engine
JAR 文件包含在类路径中。
testImplementation(platform("org.junit:junit-bom:5.10.2"))
testRuntimeOnly("org.junit.platform:junit-platform-launcher") {
because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions")
}
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
<!-- ... -->
<dependencies>
<!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.10.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
4.1.2. Eclipse
Eclipse IDE 从 Eclipse Oxygen.1a (4.7.1a) 版本开始支持 JUnit Platform。
有关在 Eclipse 中使用 JUnit 5 的更多信息,请参阅 Eclipse Project Oxygen.1a (4.7.1a) - 新功能和值得注意的更改 文档中的官方Eclipse 对 JUnit 5 的支持部分。
4.1.3. NetBeans
NetBeans 从 Apache NetBeans 10.0 版本 开始支持 JUnit Jupiter 和 JUnit Platform。
有关更多信息,请参阅 Apache NetBeans 10.0 发行说明 中的 JUnit 5 部分。
4.1.4. Visual Studio Code
Visual Studio Code 通过 Java Test Runner 扩展支持 JUnit Jupiter 和 JUnit Platform,该扩展默认情况下作为 Java Extension Pack 的一部分安装。
有关更多信息,请参阅 Java in Visual Studio Code 文档中的测试部分。
4.1.5. 其他 IDE
如果您使用的是除上述部分列出的 IDE 之外的编辑器或 IDE,JUnit 团队提供了两种替代解决方案来帮助您使用 JUnit 5。您可以手动使用 控制台启动器(例如,从命令行)或使用 基于 JUnit 4 的运行器 执行测试,如果您的 IDE 内置支持 JUnit 4。
4.2. 构建支持
4.2.1. Gradle
从 4.6 版本 开始,Gradle 提供了 对在 JUnit Platform 上执行测试的原生支持。要启用它,您需要在 build.gradle
中的 test
任务声明中指定 useJUnitPlatform()
test {
useJUnitPlatform()
}
test {
useJUnitPlatform {
includeTags("fast", "smoke & feature-a")
// excludeTags("slow", "ci")
includeEngines("junit-jupiter")
// excludeEngines("junit-vintage")
}
}
有关选项的完整列表,请参阅 官方 Gradle 文档。
对齐依赖项版本
除非您使用 Spring Boot,它定义了自己的依赖项管理方式,否则建议使用 JUnit Platform BOM 来对齐所有 JUnit 5 工件的版本。
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.2"))
}
使用 BOM 允许您在声明对所有具有 org.junit.platform
、org.junit.jupiter
和 org.junit.vintage
组 ID 的工件的依赖项时省略版本。
有关如何在 Spring Boot 应用程序中覆盖 JUnit 版本的详细信息,请参见 Spring Boot。 |
配置参数
标准 Gradle test
任务目前没有提供专门的 DSL 来设置 JUnit Platform 配置参数 以影响测试发现和执行。但是,您可以通过系统属性(如下所示)或通过 junit-platform.properties
文件在构建脚本中提供配置参数。
test {
// ...
systemProperty("junit.jupiter.conditions.deactivate", "*")
systemProperty("junit.jupiter.extensions.autodetection.enabled", true)
systemProperty("junit.jupiter.testinstance.lifecycle.default", "per_class")
// ...
}
配置测试引擎
为了运行任何测试,必须在类路径上存在 TestEngine
实现。
要配置对基于 JUnit Jupiter 的测试的支持,请配置对类似于以下内容的依赖项聚合 JUnit Jupiter 工件的 testImplementation
依赖项。
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") // version can be omitted when using the BOM
}
只要您配置了对 JUnit 4 的 testImplementation
依赖项和对 JUnit Vintage TestEngine
实现的 testRuntimeOnly
依赖项,JUnit Platform 就可以运行基于 JUnit 4 的测试,类似于以下内容。
dependencies {
testImplementation("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.2") // version can be omitted when using the BOM
}
配置日志记录(可选)
JUnit 使用 java.util.logging
包(也称为JUL)中的 Java 日志记录 API 来发出警告和调试信息。有关配置选项,请参阅 LogManager
的官方文档。
或者,可以将日志消息重定向到其他日志记录框架,例如 Log4j 或 Logback。要使用提供 LogManager
自定义实现的日志记录框架,请将 java.util.logging.manager
系统属性设置为要使用的 LogManager
实现的完全限定类名。以下示例演示了如何配置 Log4j 2.x(有关详细信息,请参见 Log4j JDK 日志记录适配器)。
test {
systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
}
其他日志记录框架提供了不同的方法来重定向使用 java.util.logging
记录的消息。例如,对于 Logback,您可以使用 JUL 到 SLF4J 桥接,方法是将额外的依赖项添加到运行时类路径。
4.2.2. Maven
从 2.22.0 版本 开始,Maven Surefire 和 Maven Failsafe 提供了 对在 JUnit Platform 上执行测试的原生支持。junit5-jupiter-starter-maven
项目中的 pom.xml
文件演示了如何使用 Maven Surefire 插件,可以作为配置 Maven 构建的起点。
使用 Maven Surefire/Failsafe 3.0.0-M4 或更高版本以避免互操作性问题
Maven Surefire/Failsafe 3.0.0-M4 引入了对将它使用的 JUnit Platform Launcher 的版本与在测试运行时类路径中找到的 JUnit Platform 版本对齐的支持。因此,建议使用 3.0.0-M4 或更高版本以避免互操作性问题。 或者,您可以将匹配版本的 JUnit Platform Launcher 的测试依赖项添加到您的 Maven 构建中,如下所示。
|
对齐依赖项版本
除非您使用 Spring Boot,它定义了自己的依赖项管理方式,否则建议使用 JUnit Platform BOM 来对齐所有 JUnit 5 工件的版本。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.10.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
使用 BOM 允许您在声明对所有具有 org.junit.platform
、org.junit.jupiter
和 org.junit.vintage
组 ID 的工件的依赖项时省略版本。
有关如何在 Spring Boot 应用程序中覆盖 JUnit 版本的详细信息,请参见 Spring Boot。 |
配置测试引擎
为了让 Maven Surefire 或 Maven Failsafe 运行任何测试,必须将至少一个 TestEngine
实现添加到测试类路径中。
要配置对基于 JUnit Jupiter 的测试的支持,请在 JUnit Jupiter API 和 JUnit Jupiter TestEngine
实现上配置 test
范围的依赖项,类似于以下示例。
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version> <!-- can be omitted when using the BOM -->
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
只要您在 JUnit 4 和 JUnit Vintage TestEngine
实现上配置 test
范围的依赖项,Maven Surefire 和 Maven Failsafe 就可以与 Jupiter 测试一起运行基于 JUnit 4 的测试,类似于以下示例。
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.10.2</version> <!-- can be omitted when using the BOM -->
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
按测试类名称筛选
Maven Surefire 插件将扫描其完全限定名称与以下模式匹配的测试类。
-
**/Test*.java
-
**/*Test.java
-
**/*Tests.java
-
**/*TestCase.java
此外,默认情况下,它将排除所有嵌套类(包括静态成员类)。
但是,请注意,您可以通过在 pom.xml
文件中配置显式的 include
和 exclude
规则来覆盖此默认行为。例如,要阻止 Maven Surefire 排除静态成员类,您可以覆盖其排除规则,如下所示。
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<excludes>
<exclude/>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
有关详细信息,请参阅 Maven Surefire 的 测试的包含和排除 文档。
按标签筛选
-
要包含标签或标签表达式,请使用
groups
。 -
要排除标签或标签表达式,请使用
excludedGroups
。
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<groups>acceptance | !feature-a</groups>
<excludedGroups>integration, regression</excludedGroups>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
配置参数
您可以设置 JUnit Platform 配置参数 来影响测试发现和执行,方法是声明 configurationParameters
属性并使用 Java Properties
文件语法(如下所示)或通过 junit-platform.properties
文件提供键值对。
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.conditions.deactivate = *
junit.jupiter.extensions.autodetection.enabled = true
junit.jupiter.testinstance.lifecycle.default = per_class
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
4.2.3. Ant
从版本 1.10.3
开始,Ant 具有一个 junitlauncher
任务,该任务提供对在 JUnit Platform 上启动测试的原生支持。junitlauncher
任务仅负责启动 JUnit Platform 并将选定的测试集合传递给它。然后,JUnit Platform 将委托给已注册的测试引擎来发现和执行测试。
junitlauncher
任务尝试尽可能地与原生 Ant 结构(例如 资源集合)保持一致,以允许用户选择他们希望由测试引擎执行的测试。与许多其他核心 Ant 任务相比,这使该任务具有始终如一且自然的体验。
从 Ant 版本 1.10.6
开始,junitlauncher
任务支持 在单独的 JVM 中分叉测试。
junit5-jupiter-starter-ant
项目中的 build.xml
文件演示了如何使用该任务,并且可以作为起点。
基本用法
以下示例演示了如何配置 junitlauncher
任务以选择单个测试类(即 org.myapp.test.MyFirstJUnit5Test
)。
<path id="test.classpath">
<!-- The location where you have your compiled classes -->
<pathelement location="${build.classes.dir}" />
</path>
<!-- ... -->
<junitlauncher>
<classpath refid="test.classpath" />
<test name="org.myapp.test.MyFirstJUnit5Test" />
</junitlauncher>
test
元素允许您指定要选择和执行的单个测试类。classpath
元素允许您指定用于启动 JUnit Platform 的类路径。此类路径还将用于查找作为执行一部分的测试类。
以下示例演示了如何配置 junitlauncher
任务以从多个位置选择测试类。
<path id="test.classpath">
<!-- The location where you have your compiled classes -->
<pathelement location="${build.classes.dir}" />
</path>
<!-- ... -->
<junitlauncher>
<classpath refid="test.classpath" />
<testclasses outputdir="${output.dir}">
<fileset dir="${build.classes.dir}">
<include name="org/example/**/demo/**/" />
</fileset>
<fileset dir="${some.other.dir}">
<include name="org/myapp/**/" />
</fileset>
</testclasses>
</junitlauncher>
在上面的示例中,testclasses
元素允许您选择位于不同位置的多个测试类。
有关用法和配置选项的更多详细信息,请参阅 junitlauncher
任务 的官方 Ant 文档。
4.2.4. Spring Boot
Spring Boot 提供对管理项目中使用的 JUnit 版本的自动支持。此外,spring-boot-starter-test
工件会自动包含测试库,例如 JUnit Jupiter、AssertJ、Mockito 等。
如果您的构建依赖于 Spring Boot 的依赖项管理支持,则不应在构建脚本中导入 junit-bom
,因为这会导致重复(并且可能冲突)的 JUnit 依赖项管理。
如果您需要覆盖 Spring Boot 应用程序中使用的依赖项的版本,则必须覆盖 Spring Boot 插件使用的 BOM 中定义的 版本属性 的确切名称。例如,Spring Boot 中的 JUnit Jupiter 版本属性的名称是 junit-jupiter.version
。更改依赖项版本的机制在 Gradle 和 Maven 中都有记录。
使用 Gradle,您可以通过在 build.gradle
文件中包含以下内容来覆盖 JUnit Jupiter 版本。
ext['junit-jupiter.version'] = '5.10.2'
使用 Maven,您可以通过在 pom.xml
文件中包含以下内容来覆盖 JUnit Jupiter 版本。
<properties>
<junit-jupiter.version>5.10.2</junit-jupiter.version>
</properties>
4.3. 控制台启动器
ConsoleLauncher
是一个命令行 Java 应用程序,允许您从控制台启动 JUnit Platform。例如,它可以用于运行 JUnit Vintage 和 JUnit Jupiter 测试并将测试执行结果打印到控制台。
包含所有依赖项的可执行 junit-platform-console-standalone-1.10.2.jar
发布在 Maven Central 存储库的 junit-platform-console-standalone 目录下。它包含以下依赖项
-
junit:junit:4.13.2
-
org.apiguardian:apiguardian-api:1.1.2
-
org.hamcrest:hamcrest-core:1.3
-
org.junit.jupiter:junit-jupiter-api:5.10.2
-
org.junit.jupiter:junit-jupiter-engine:5.10.2
-
org.junit.jupiter:junit-jupiter-params:5.10.2
-
org.junit.platform:junit-platform-commons:1.10.2
-
org.junit.platform:junit-platform-console:1.10.2
-
org.junit.platform:junit-platform-engine:1.10.2
-
org.junit.platform:junit-platform-launcher:1.10.2
-
org.junit.platform:junit-platform-reporting:1.10.2
-
org.junit.platform:junit-platform-suite-api:1.10.2
-
org.junit.platform:junit-platform-suite-commons:1.10.2
-
org.junit.platform:junit-platform-suite-engine:1.10.2
-
org.junit.platform:junit-platform-suite:1.10.2
-
org.junit.vintage:junit-vintage-engine:5.10.2
-
org.opentest4j:opentest4j:1.3.0
您可以 运行 独立的 ConsoleLauncher
,如下所示。
$ java -jar junit-platform-console-standalone-1.10.2.jar execute <OPTIONS>
├─ JUnit Vintage
│ └─ example.JUnit4Tests
│ └─ standardJUnit4Test ✔
└─ JUnit Jupiter
├─ StandardTests
│ ├─ succeedingTest() ✔
│ └─ skippedTest() ↷ for demonstration purposes
└─ A special test case
├─ Custom test name containing spaces ✔
├─ ╯°□°)╯ ✔
└─ 😱 ✔
Test run finished after 64 ms
[ 5 containers found ]
[ 0 containers skipped ]
[ 5 containers started ]
[ 0 containers aborted ]
[ 5 containers successful ]
[ 0 containers failed ]
[ 6 tests found ]
[ 1 tests skipped ]
[ 5 tests started ]
[ 0 tests aborted ]
[ 5 tests successful ]
[ 0 tests failed ]
您也可以运行独立的 ConsoleLauncher
,如下所示(例如,要包含目录中的所有 jar 文件)
$ java -cp classes:testlib/* org.junit.platform.console.ConsoleLauncher <OPTIONS>
退出代码 如果任何容器或测试失败,ConsoleLauncher 将以状态代码 1 退出。如果未发现任何测试并且提供了 --fail-if-no-tests 命令行选项,则 ConsoleLauncher 将以状态代码 2 退出。否则,退出代码为 0 。 |
4.3.1. 子命令和选项
ConsoleLauncher
提供以下子命令
Usage: junit [OPTIONS] [COMMAND] Launches the JUnit Platform for test discovery and execution. [@<filename>...] One or more argument files containing options. Commands: discover Discover tests execute Execute tests engines List available test engines For more information, please refer to the JUnit User Guide at https://junit.java.net.cn/junit5/docs/current/user-guide/
发现测试
Usage: junit discover [OPTIONS] Discover tests [@<filename>...] One or more argument files containing options. --disable-banner Disable print out of the welcome message. --disable-ansi-colors Disable ANSI colors in output (not supported by all terminals). -h, --help Display help information. SELECTORS --scan-classpath, --scan-class-path[=PATH] Scan all directories on the classpath or explicit classpath roots. Without arguments, only directories on the system classpath as well as additional classpath entries supplied via -cp (directories and JAR files) are scanned. Explicit classpath roots that are not on the classpath will be silently ignored. This option can be repeated. --scan-modules Scan all resolved modules for test discovery. -u, --select-uri=URI Select a URI for test discovery. This option can be repeated. -f, --select-file=FILE Select a file for test discovery. This option can be repeated. -d, --select-directory=DIR Select a directory for test discovery. This option can be repeated. -o, --select-module=NAME Select single module for test discovery. This option can be repeated. -p, --select-package=PKG Select a package for test discovery. This option can be repeated. -c, --select-class=CLASS Select a class for test discovery. This option can be repeated. -m, --select-method=NAME Select a method for test discovery. This option can be repeated. -r, --select-resource=RESOURCE Select a classpath resource for test discovery. This option can be repeated. -i, --select-iteration=TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*] Select iterations for test discovery (e.g. method:com.acme.Foo#m() [1..2]). This option can be repeated. FILTERS -n, --include-classname=PATTERN Provide a regular expression to include only classes whose fully qualified names match. To avoid loading classes unnecessarily, the default pattern only includes class names that begin with "Test" or end with "Test" or "Tests". When this option is repeated, all patterns will be combined using OR semantics. Default: ^(Test.*|.+[.$]Test.*|.*Tests?)$ -N, --exclude-classname=PATTERN Provide a regular expression to exclude those classes whose fully qualified names match. When this option is repeated, all patterns will be combined using OR semantics. --include-package=PKG Provide a package to be included in the test run. This option can be repeated. --exclude-package=PKG Provide a package to be excluded from the test run. This option can be repeated. -t, --include-tag=TAG Provide a tag or tag expression to include only tests whose tags match. When this option is repeated, all patterns will be combined using OR semantics. -T, --exclude-tag=TAG Provide a tag or tag expression to exclude those tests whose tags match. When this option is repeated, all patterns will be combined using OR semantics. -e, --include-engine=ID Provide the ID of an engine to be included in the test run. This option can be repeated. -E, --exclude-engine=ID Provide the ID of an engine to be excluded from the test run. This option can be repeated. RUNTIME CONFIGURATION -cp, --classpath, --class-path=PATH Provide additional classpath entries -- for example, for adding engines and their dependencies. This option can be repeated. --config=KEY=VALUE Set a configuration parameter for test discovery and execution. This option can be repeated. CONSOLE OUTPUT --color-palette=FILE Specify a path to a properties file to customize ANSI style of output (not supported by all terminals). --single-color Style test output using only text attributes, no color (not supported by all terminals). --details=MODE Select an output details mode for when tests are executed. Use one of: none, summary, flat, tree, verbose, testfeed. If 'none' is selected, then only the summary and test failures are shown. Default: tree. --details-theme=THEME Select an output details tree theme for when tests are executed. Use one of: ascii, unicode. Default is detected based on default character encoding. For more information, please refer to the JUnit User Guide at https://junit.java.net.cn/junit5/docs/current/user-guide/
执行测试
Usage: junit execute [OPTIONS] Execute tests [@<filename>...] One or more argument files containing options. --disable-banner Disable print out of the welcome message. --disable-ansi-colors Disable ANSI colors in output (not supported by all terminals). -h, --help Display help information. SELECTORS --scan-classpath, --scan-class-path[=PATH] Scan all directories on the classpath or explicit classpath roots. Without arguments, only directories on the system classpath as well as additional classpath entries supplied via -cp (directories and JAR files) are scanned. Explicit classpath roots that are not on the classpath will be silently ignored. This option can be repeated. --scan-modules Scan all resolved modules for test discovery. -u, --select-uri=URI Select a URI for test discovery. This option can be repeated. -f, --select-file=FILE Select a file for test discovery. This option can be repeated. -d, --select-directory=DIR Select a directory for test discovery. This option can be repeated. -o, --select-module=NAME Select single module for test discovery. This option can be repeated. -p, --select-package=PKG Select a package for test discovery. This option can be repeated. -c, --select-class=CLASS Select a class for test discovery. This option can be repeated. -m, --select-method=NAME Select a method for test discovery. This option can be repeated. -r, --select-resource=RESOURCE Select a classpath resource for test discovery. This option can be repeated. -i, --select-iteration=TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*] Select iterations for test discovery (e.g. method:com.acme.Foo#m() [1..2]). This option can be repeated. FILTERS -n, --include-classname=PATTERN Provide a regular expression to include only classes whose fully qualified names match. To avoid loading classes unnecessarily, the default pattern only includes class names that begin with "Test" or end with "Test" or "Tests". When this option is repeated, all patterns will be combined using OR semantics. Default: ^(Test.*|.+[.$]Test.*|.*Tests?)$ -N, --exclude-classname=PATTERN Provide a regular expression to exclude those classes whose fully qualified names match. When this option is repeated, all patterns will be combined using OR semantics. --include-package=PKG Provide a package to be included in the test run. This option can be repeated. --exclude-package=PKG Provide a package to be excluded from the test run. This option can be repeated. -t, --include-tag=TAG Provide a tag or tag expression to include only tests whose tags match. When this option is repeated, all patterns will be combined using OR semantics. -T, --exclude-tag=TAG Provide a tag or tag expression to exclude those tests whose tags match. When this option is repeated, all patterns will be combined using OR semantics. -e, --include-engine=ID Provide the ID of an engine to be included in the test run. This option can be repeated. -E, --exclude-engine=ID Provide the ID of an engine to be excluded from the test run. This option can be repeated. RUNTIME CONFIGURATION -cp, --classpath, --class-path=PATH Provide additional classpath entries -- for example, for adding engines and their dependencies. This option can be repeated. --config=KEY=VALUE Set a configuration parameter for test discovery and execution. This option can be repeated. CONSOLE OUTPUT --color-palette=FILE Specify a path to a properties file to customize ANSI style of output (not supported by all terminals). --single-color Style test output using only text attributes, no color (not supported by all terminals). --details=MODE Select an output details mode for when tests are executed. Use one of: none, summary, flat, tree, verbose, testfeed. If 'none' is selected, then only the summary and test failures are shown. Default: tree. --details-theme=THEME Select an output details tree theme for when tests are executed. Use one of: ascii, unicode. Default is detected based on default character encoding. REPORTING --fail-if-no-tests Fail and return exit status code 2 if no tests are found. --reports-dir=DIR Enable report output into a specified local directory (will be created if it does not exist). For more information, please refer to the JUnit User Guide at https://junit.java.net.cn/junit5/docs/current/user-guide/
列出测试引擎
Usage: junit engines [OPTIONS] List available test engines [@<filename>...] One or more argument files containing options. --disable-banner Disable print out of the welcome message. --disable-ansi-colors Disable ANSI colors in output (not supported by all terminals). -h, --help Display help information. For more information, please refer to the JUnit User Guide at https://junit.java.net.cn/junit5/docs/current/user-guide/
4.3.2. 参数文件(@-文件)
在某些平台上,当创建包含大量选项或长参数的命令行时,您可能会遇到命令行长度的系统限制。
从版本 1.3 开始,ConsoleLauncher
支持参数文件,也称为@-文件。参数文件是本身包含要传递给命令的参数的文件。当底层的 picocli 命令行解析器遇到以字符 @
开头的参数时,它会将该文件的内容扩展到参数列表中。
文件中的参数可以用空格或换行符分隔。如果参数包含嵌入的空格,则整个参数应包含在双引号或单引号中,例如 "-f=My Files/Stuff.java"
。
如果参数文件不存在或无法读取,则该参数将被视为字面量,并且不会被删除。这很可能会导致“不匹配的参数”错误消息。您可以通过使用 picocli.trace
系统属性设置为 DEBUG
来执行命令来排查此类错误。
可以在命令行上指定多个@-文件。指定的路径可以相对于当前目录或绝对路径。
您可以通过使用额外的 @
符号转义来传递以初始 @
字符开头的实际参数。例如,@@somearg
将变为 @somearg
,并且不会被扩展。
4.3.3. 颜色自定义
ConsoleLauncher
输出中使用的颜色可以自定义。选项 --single-color
将应用内置单色样式,而 --color-palette
将接受一个属性文件来覆盖 ANSI SGR 颜色样式。以下属性文件演示了默认样式
SUCCESSFUL = 32
ABORTED = 33
FAILED = 31
SKIPPED = 35
CONTAINER = 35
TEST = 34
DYNAMIC = 35
REPORTED = 37
4.4. 使用 JUnit 4 运行 JUnit Platform
JUnitPlatform 运行器已弃用
近年来,所有主流构建工具和 IDE 都提供对直接在 JUnit Platform 上运行测试的内置支持。 此外, 因此, 如果您正在使用 |
JUnitPlatform
运行器是一个基于 JUnit 4 的 Runner
,它使您能够在 JUnit 4 环境中运行任何其编程模型在 JUnit Platform 上受支持的测试,例如 JUnit Jupiter 测试类。
使用 `@RunWith(JUnitPlatform.class)` 注解类,可以让它在支持 JUnit 4 但尚未直接支持 JUnit Platform 的 IDE 和构建系统中运行。
由于 JUnit Platform 具有 JUnit 4 不具备的功能,因此该运行器只能支持 JUnit Platform 功能的一个子集,尤其是在报告方面(参见 显示名称与技术名称)。 |
4.4.1. 设置
您需要在类路径中包含以下工件及其依赖项。有关组 ID、工件 ID 和版本的详细信息,请参见 依赖项元数据。
4.4.2. 显示名称与技术名称
要为通过 `@RunWith(JUnitPlatform.class)` 注解运行的类定义自定义显示名称,请使用 `@SuiteDisplayName` 注解该类并提供自定义值。
默认情况下,显示名称将用于测试工件;但是,当使用 `JUnitPlatform` 运行器使用 Gradle 或 Maven 等构建工具执行测试时,生成的测试报告通常需要包含测试工件的技术名称(例如,完全限定的类名),而不是更短的显示名称,例如测试类的简单名称或包含特殊字符的自定义显示名称。要为报告目的启用技术名称,请在 `@RunWith(JUnitPlatform.class)` 旁边声明 `@UseTechnicalNames` 注解。
请注意,`@UseTechnicalNames` 的存在会覆盖通过 `@SuiteDisplayName` 配置的任何自定义显示名称。
4.4.3. 单个测试类
使用 `JUnitPlatform` 运行器的一种方法是直接使用 `@RunWith(JUnitPlatform.class)` 注解测试类。请注意,以下示例中的测试方法使用 `org.junit.jupiter.api.Test`(JUnit Jupiter)注解,而不是 `org.junit.Test`(JUnit 4)注解。此外,在这种情况下,测试类必须是 `public`;否则,某些 IDE 和构建工具可能无法将其识别为 JUnit 4 测试类。
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
@RunWith(org.junit.platform.runner.JUnitPlatform.class)
public class JUnitPlatformClassDemo {
@Test
void succeedingTest() {
/* no-op */
}
@Test
void failingTest() {
fail("Failing for failing's sake.");
}
}
4.4.4. 测试套件
如果您有多个测试类,您可以创建测试套件,如以下示例所示。
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.SuiteDisplayName;
import org.junit.runner.RunWith;
@RunWith(org.junit.platform.runner.JUnitPlatform.class)
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
public class JUnitPlatformSuiteDemo {
}
`JUnitPlatformSuiteDemo` 将发现并运行 `example` 包及其子包中的所有测试。默认情况下,它只包含名称以 `Test` 开头或以 `Test` 或 `Tests` 结尾的测试类。
其他配置选项 除了 `@SelectPackages` 之外,还有更多用于发现和过滤测试的配置选项。有关更多详细信息,请参阅 `org.junit.platform.suite.api` 包的 Javadoc。 |
使用 `@RunWith(JUnitPlatform.class)` 注解的测试类和套件**不能**直接在 JUnit Platform 上执行(或作为某些 IDE 中记录的“JUnit 5”测试)。此类类和套件只能使用 JUnit 4 基础设施执行。 |
4.5. 配置参数
除了指示平台要包含哪些测试类和测试引擎、要扫描哪些包等之外,有时还需要提供特定于特定测试引擎、监听器或注册扩展的附加自定义配置参数。例如,JUnit Jupiter `TestEngine` 支持以下用例的配置参数。
配置参数是基于文本的键值对,可以通过以下机制之一提供给在 JUnit Platform 上运行的测试引擎。
-
`LauncherDiscoveryRequestBuilder` 中的 `configurationParameter()` 和 `configurationParameters()` 方法,用于构建提供给 `Launcher` API 的请求。当通过 JUnit Platform 提供的工具之一运行测试时,您可以按如下方式指定配置参数
-
控制台启动器:使用 `--config` 命令行选项。
-
Gradle:使用 `systemProperty` 或 `systemProperties` DSL。
-
Maven Surefire 提供程序:使用 `configurationParameters` 属性。
-
-
JVM 系统属性。
-
JUnit Platform 配置文件:类路径根目录中的名为 `junit-platform.properties` 的文件,遵循 Java `Properties` 文件的语法规则。
配置参数按上述定义的顺序查找。因此,直接提供给 `Launcher` 的配置参数优先于通过系统属性和配置文件提供的配置参数。类似地,通过系统属性提供的配置参数优先于通过配置文件提供的配置参数。 |
4.5.1. 模式匹配语法
本节介绍应用于以下功能使用的配置参数的模式匹配语法。
如果给定配置参数的值仅包含一个星号 (*
),则该模式将匹配所有候选类。否则,该值将被视为一个逗号分隔的模式列表,其中每个模式将与每个候选类的完全限定类名 (FQCN) 匹配。模式中的任何点 (.
) 将匹配 FQCN 中的点 (.
) 或美元符号 ($
)。任何星号 (*
) 将匹配 FQCN 中的一个或多个字符。模式中的所有其他字符将一对一地与 FQCN 匹配。
示例
-
*
:匹配所有候选类。 -
org.junit.*
:匹配 `org.junit` 基本包及其任何子包下的所有候选类。 -
*.MyCustomImpl
:匹配每个简单类名恰好为 `MyCustomImpl` 的候选类。 -
*System*
:匹配每个 FQCN 包含 `System` 的候选类。 -
*System*, *Unit*
:匹配每个 FQCN 包含 `System` 或 `Unit` 的候选类。 -
org.example.MyCustomImpl
:匹配 FQCN 恰好为 `org.example.MyCustomImpl` 的候选类。 -
org.example.MyCustomImpl, org.example.TheirCustomImpl
:匹配 FQCN 恰好为 `org.example.MyCustomImpl` 或 `org.example.TheirCustomImpl` 的候选类。
4.6. 标签
标签是 JUnit Platform 用于标记和过滤测试的概念。向容器和测试添加标签的编程模型由测试框架定义。例如,在基于 JUnit Jupiter 的测试中,应使用 `@Tag` 注解(参见 标记和过滤)。对于基于 JUnit 4 的测试,Vintage 引擎将 `@Category` 注解映射到标签(参见 类别支持)。其他测试框架可能会定义自己的注解或其他方法供用户指定标签。
4.6.1. 标签语法规则
无论如何指定标签,JUnit Platform 都强制执行以下规则
-
标签不能为 `null` 或空白。
-
修剪后的标签不能包含空格。
-
修剪后的标签不能包含 ISO 控制字符。
-
修剪后的标签不能包含以下任何保留字符。
-
,
:逗号 -
(
:左括号 -
)
:右括号 -
&
:与号 -
|
:竖线 -
!
:感叹号
-
在上述上下文中,“修剪”表示已删除前导和尾随空格字符。 |
4.6.2. 标签表达式
标签表达式是使用运算符 `!`、`&` 和 `|` 的布尔表达式。此外,可以使用 `( `和 `) `来调整运算符优先级。
支持两种特殊表达式,`any()` 和 `none()`,它们分别选择所有具有任何标签的测试和所有没有任何标签的测试。这些特殊表达式可以像普通标签一样与其他表达式组合。
运算符 | 含义 | 结合性 |
---|---|---|
|
not |
右 |
|
and |
左 |
|
or |
左 |
左
如果您在多个维度上标记测试,标签表达式可以帮助您选择要执行的测试。当按测试类型(例如,微型、集成、端到端)和功能(例如,产品、目录、运输)进行标记时,以下标签表达式可能会有用。 | 标签表达式 |
---|---|
|
product |
|
catalog | shipping |
|
catalog & shipping |
|
product & !end-to-end |
|
(micro | integration) & (product | shipping) |
产品或运输的所有微型或集成测试
如果启用,JUnit Platform 会捕获相应的输出,并使用stdout
或stderr
键将其发布为报告条目,以便在报告测试或容器已完成之前立即将其发布到所有已注册的TestExecutionListener
实例。
请注意,捕获的输出将仅包含由用于执行容器或测试的线程发出的输出。其他线程的任何输出都将被省略,因为特别是在并行执行测试时,不可能将其归因于特定测试或容器。
4.8. 使用监听器和拦截器
JUnit Platform 提供以下监听器 API,允许 JUnit、第三方和自定义用户代码对在TestPlan
的发现和执行期间的不同点触发的事件做出反应。
-
LauncherSessionListener
:在LauncherSession
打开和关闭时接收事件。 -
LauncherInterceptor
:在LauncherSession
的上下文中拦截测试发现和执行。 -
LauncherDiscoveryListener
:接收在测试发现期间发生的事件。 -
TestExecutionListener
:接收在测试执行期间发生的事件。
LauncherSessionListener
API 通常由构建工具或 IDE 实现,并自动为您注册,以支持构建工具或 IDE 的某些功能。
LauncherDiscoveryListener
和TestExecutionListener
API 通常是为了生成某种形式的报告或在 IDE 中显示测试计划的图形表示而实现的。此类监听器可以由构建工具或 IDE 实现并自动注册,也可以包含在第三方库中,并可能自动为您注册。您也可以实现和注册自己的监听器。
有关注册和配置监听器的详细信息,请参阅本指南的以下部分。
JUnit Platform 提供以下监听器,您可能希望将其与您的测试套件一起使用。
- JUnit Platform 报告
-
LegacyXmlReportGeneratingListener
可以通过控制台启动器使用,也可以手动注册以生成与基于 JUnit 4 的测试报告的事实标准兼容的 XML 报告。OpenTestReportGeneratingListener
根据Open Test Reporting中指定的基于事件的格式生成 XML 报告。它会自动注册,并且可以通过配置参数启用和配置。有关详细信息,请参阅JUnit Platform 报告。
- 飞行记录器支持
-
FlightRecordingExecutionListener
和FlightRecordingDiscoveryListener
在测试发现和执行期间生成 Java Flight Recorder 事件。 LoggingListener
-
TestExecutionListener
用于通过BiConsumer
记录所有事件的信息消息,该BiConsumer
使用Throwable
和Supplier<String>
。 SummaryGeneratingListener
-
TestExecutionListener
,它生成测试执行的摘要,可以通过PrintWriter
打印。 UniqueIdTrackingListener
-
TestExecutionListener
,它跟踪在TestPlan
执行期间跳过或执行的所有测试的唯一 ID,并在TestPlan
执行完成后生成包含唯一 ID 的文件。
4.8.1. 飞行记录器支持
从 1.7 版本开始,JUnit Platform 提供了对生成飞行记录器事件的可选支持。JEP 328将 Java Flight Recorder (JFR) 描述为
飞行记录器记录来自应用程序、JVM 和操作系统的事件。事件存储在一个文件中,可以附加到错误报告中并由支持工程师检查,从而允许在问题发生之前对问题进行事后分析。 |
为了记录在运行测试时生成的飞行记录器事件,您需要
-
确保您使用的是 Java 8 Update 262 或更高版本,或者 Java 11 或更高版本。
-
在测试运行时将
org.junit.platform.jfr
模块(junit-platform-jfr-1.10.2.jar
)提供在类路径或模块路径上。 -
在启动测试运行时启动飞行记录。飞行记录器可以通过 java 命令行选项启动
-XX:StartFlightRecording:filename=...
请查阅您的构建工具手册以获取相应的命令。
要分析记录的事件,请使用最近 JDK 附带的jfr命令行工具,或使用JDK Mission Control打开记录文件。
飞行记录器支持目前是一个实验性功能。欢迎您试用并向 JUnit 团队提供反馈,以便他们可以改进并最终推广此功能。 |
4.9. 堆栈跟踪修剪
从 1.10 版本开始,JUnit Platform 提供了对修剪失败测试产生的堆栈跟踪的内置支持。此功能默认启用,但可以通过将junit.platform.stacktrace.pruning.enabled
配置参数设置为false
来禁用。
启用后,将从堆栈跟踪中删除来自org.junit
、jdk.internal.reflect
和sun.reflect
包的所有调用,除非这些调用发生在测试本身或其任何祖先之后。因此,对org.junit.jupiter.api.Assertions
或org.junit.jupiter.api.Assumptions
的调用将永远不会被排除。
此外,将删除在第一个来自 JUnit Platform Launcher 的调用之前和包括该调用的所有元素。
5. 扩展模型
5.1. 概述
与 JUnit 4 中竞争的Runner
、TestRule
和MethodRule
扩展点相比,JUnit Jupiter 扩展模型包含一个单一、连贯的概念:Extension
API。但是,请注意,Extension
本身只是一个标记接口。
5.2. 注册扩展
扩展可以通过@ExtendWith
声明式注册,可以通过@RegisterExtension
以编程方式注册,或者可以通过 Java 的ServiceLoader
机制自动注册。
5.2.1. 声明式扩展注册
开发人员可以通过使用@ExtendWith(…)
注释测试接口、测试类、测试方法或自定义组合注释,并为要注册的扩展提供类引用,来声明式注册一个或多个扩展。从 JUnit Jupiter 5.8 开始,@ExtendWith
也可以在测试类构造函数中的字段或参数上,在测试方法中,以及在@BeforeAll
、@AfterAll
、@BeforeEach
和@AfterEach
生命周期方法中声明。
例如,要为特定测试方法注册WebServerExtension
,您需要按如下方式注释测试方法。我们假设WebServerExtension
启动一个本地 Web 服务器,并将服务器的 URL 注入到用@WebServerUrl
注释的参数中。
@Test
@ExtendWith(WebServerExtension.class)
void getProductList(@WebServerUrl String serverUrl) {
WebClient webClient = new WebClient();
// Use WebClient to connect to web server using serverUrl and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
要为特定类及其子类中的所有测试注册WebServerExtension
,您需要按如下方式注释测试类。
@ExtendWith(WebServerExtension.class)
class MyTests {
// ...
}
可以像这样一起注册多个扩展
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
class MyFirstTests {
// ...
}
或者,可以像这样分别注册多个扩展
@ExtendWith(DatabaseExtension.class)
@ExtendWith(WebServerExtension.class)
class MySecondTests {
// ...
}
扩展注册顺序
在类级别、方法级别或参数级别通过 |
如果您希望以可重用方式组合多个扩展,则可以定义一个自定义组合注释,并在以下代码清单中使用@ExtendWith
作为元注释。然后,@DatabaseAndWebServerExtension
可以代替@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
使用。
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
public @interface DatabaseAndWebServerExtension {
}
以上示例演示了如何在类级别或方法级别应用@ExtendWith
;但是,对于某些用例,扩展在字段或参数级别声明式注册是有意义的。考虑一个RandomNumberExtension
,它生成随机数,可以将其注入字段,或者通过构造函数、测试方法或生命周期方法中的参数注入。如果扩展提供了一个用@ExtendWith(RandomNumberExtension.class)
(见下文清单)元注释的@Random
注释,则扩展可以像以下RandomNumberDemo
示例中那样透明地使用。
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
class RandomNumberDemo {
// Use static randomNumber0 field anywhere in the test class,
// including @BeforeAll or @AfterEach lifecycle methods.
@Random
private static Integer randomNumber0;
// Use randomNumber1 field in test methods and @BeforeEach
// or @AfterEach lifecycle methods.
@Random
private int randomNumber1;
RandomNumberDemo(@Random int randomNumber2) {
// Use randomNumber2 in constructor.
}
@BeforeEach
void beforeEach(@Random int randomNumber3) {
// Use randomNumber3 in @BeforeEach method.
}
@Test
void test(@Random int randomNumber4) {
// Use randomNumber4 in test method.
}
}
以下代码清单提供了一个关于如何实现RandomNumberExtension
的示例。此实现适用于RandomNumberDemo
中的用例;但是,它可能不足以涵盖所有用例,例如,随机数生成支持仅限于整数;它使用java.util.Random
而不是java.security.SecureRandom
;等等。无论如何,重要的是要注意实现了哪些扩展 API 以及原因。
具体来说,RandomNumberExtension
实现了以下扩展 API
-
BeforeAllCallback
:支持静态字段注入 -
BeforeEachCallback
:支持非静态字段注入 -
ParameterResolver
:支持构造函数和方法注入
理想情况下, 但是,JUnit Jupiter 目前不允许通过 |
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedFields;
import java.lang.reflect.Field;
import java.util.function.Predicate;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.ModifierSupport;
class RandomNumberExtension
implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {
private final java.util.Random random = new java.util.Random(System.nanoTime());
/**
* Inject a random integer into static fields that are annotated with
* {@code @Random} and can be assigned an integer value.
*/
@Override
public void beforeAll(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
injectFields(testClass, null, ModifierSupport::isStatic);
}
/**
* Inject a random integer into non-static fields that are annotated with
* {@code @Random} and can be assigned an integer value.
*/
@Override
public void beforeEach(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
Object testInstance = context.getRequiredTestInstance();
injectFields(testClass, testInstance, ModifierSupport::isNotStatic);
}
/**
* Determine if the parameter is annotated with {@code @Random} and can be
* assigned an integer value.
*/
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
return pc.isAnnotated(Random.class) && isInteger(pc.getParameter().getType());
}
/**
* Resolve a random integer.
*/
@Override
public Integer resolveParameter(ParameterContext pc, ExtensionContext ec) {
return this.random.nextInt();
}
private void injectFields(Class<?> testClass, Object testInstance,
Predicate<Field> predicate) {
predicate = predicate.and(field -> isInteger(field.getType()));
findAnnotatedFields(testClass, Random.class, predicate)
.forEach(field -> {
try {
field.setAccessible(true);
field.set(testInstance, this.random.nextInt());
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
});
}
private static boolean isInteger(Class<?> type) {
return type == Integer.class || type == int.class;
}
}
@ExtendWith 在字段上的扩展注册顺序通过 |
5.2.2. 以编程方式注册扩展
开发人员可以通过在测试类中的字段上使用@RegisterExtension
注释来以编程方式注册扩展。
当通过@ExtendWith
声明式注册扩展时,通常只能通过注释对其进行配置。相反,当通过@RegisterExtension
注册扩展时,可以以编程方式对其进行配置,例如,为了向扩展的构造函数、静态工厂方法或构建器 API 传递参数。
扩展注册顺序
默认情况下,通过 任何未用 |
@RegisterExtension 字段不能为null (在评估时),但可以是static 或非静态的。 |
静态字段
如果@RegisterExtension
字段是static
,则该扩展将在通过@ExtendWith
在类级别注册的扩展之后注册。这种静态扩展不受其可以实现的扩展 API 的限制。因此,通过静态字段注册的扩展可以实现类级别和实例级别的扩展 API,例如BeforeAllCallback
、AfterAllCallback
、TestInstancePostProcessor
和TestInstancePreDestroyCallback
,以及方法级别的扩展 API,例如BeforeEachCallback
等。
在以下示例中,测试类中的server
字段通过使用WebServerExtension
支持的构建器模式以编程方式初始化。配置后的WebServerExtension
将自动注册为类级别的扩展,例如,为了在类中的所有测试之前启动服务器,然后在类中的所有测试完成后停止服务器。此外,使用@BeforeAll
或@AfterAll
注释的静态生命周期方法以及@BeforeEach
、@AfterEach
和@Test
方法可以根据需要通过server
字段访问扩展的实例。
class WebServerDemo {
@RegisterExtension
static WebServerExtension server = WebServerExtension.builder()
.enableSecurity(false)
.build();
@Test
void getProductList() {
WebClient webClient = new WebClient();
String serverUrl = server.getServerUrl();
// Use WebClient to connect to web server using serverUrl and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
}
Kotlin 中的静态字段
Kotlin 编程语言没有static
字段的概念。但是,可以使用 Kotlin 中的@JvmStatic
注释指示编译器生成一个private static
字段。如果希望 Kotlin 编译器生成一个public static
字段,可以使用@JvmField
注释。
以下示例是上一节中WebServerDemo
的版本,已移植到 Kotlin。
class KotlinWebServerDemo {
companion object {
@JvmStatic
@RegisterExtension
val server = WebServerExtension.builder()
.enableSecurity(false)
.build()
}
@Test
fun getProductList() {
// Use WebClient to connect to web server using serverUrl and verify response
val webClient = WebClient()
val serverUrl = server.serverUrl
assertEquals(200, webClient.get("$serverUrl/products").responseStatus)
}
}
实例字段
如果@RegisterExtension
字段是非静态的(即实例字段),则该扩展将在测试类实例化后以及每个注册的TestInstancePostProcessor
有机会对测试实例进行后处理(可能将要使用的扩展实例注入到注释字段中)之后注册。因此,如果这种实例扩展实现类级别或实例级别的扩展 API,例如BeforeAllCallback
、AfterAllCallback
或TestInstancePostProcessor
,则不会遵守这些 API。默认情况下,实例扩展将在通过@ExtendWith
在方法级别注册的扩展之后注册;但是,如果测试类配置了@TestInstance(Lifecycle.PER_CLASS)
语义,则实例扩展将在通过@ExtendWith
在方法级别注册的扩展之前注册。
在以下示例中,测试类中的docs
字段通过调用自定义lookUpDocsDir()
方法并将结果提供给DocumentationExtension
中的静态forPath()
工厂方法以编程方式初始化。配置后的DocumentationExtension
将自动注册为方法级别的扩展。此外,@BeforeEach
、@AfterEach
和@Test
方法可以根据需要通过docs
字段访问扩展的实例。
class DocumentationDemo {
static Path lookUpDocsDir() {
// return path to docs dir
}
@RegisterExtension
DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir());
@Test
void generateDocumentation() {
// use this.docs ...
}
}
5.2.3. 自动扩展注册
除了使用注释的声明式扩展注册和以编程方式注册扩展支持之外,JUnit Jupiter 还通过 Java 的ServiceLoader
机制支持全局扩展注册,允许根据类路径中可用的内容自动检测和自动注册第三方扩展。
具体来说,可以通过在包含 JAR 文件的/META-INF/services
文件夹中名为org.junit.jupiter.api.extension.Extension
的文件中提供其完全限定的类名来注册自定义扩展。
启用自动扩展检测
自动检测是一项高级功能,因此默认情况下未启用。要启用它,请将junit.jupiter.extensions.autodetection.enabled
配置参数设置为true
。这可以作为 JVM 系统属性提供,作为传递给Launcher
的LauncherDiscoveryRequest
中的配置参数,或者通过 JUnit Platform 配置文件提供(有关详细信息,请参阅配置参数)。
例如,要启用扩展的自动检测,可以使用以下系统属性启动 JVM。
-Djunit.jupiter.extensions.autodetection.enabled=true
启用自动检测后,通过ServiceLoader
机制发现的扩展将在 JUnit Jupiter 的全局扩展(例如,对TestInfo
、TestReporter
等的支持)之后添加到扩展注册表中。
5.3. 条件测试执行
ExecutionCondition
定义了用于以编程方式进行条件测试执行的Extension
API。
ExecutionCondition
针对每个容器(例如,测试类)进行评估,以确定根据提供的ExtensionContext
是否应执行其包含的所有测试。类似地,ExecutionCondition
针对每个测试进行评估,以确定根据提供的ExtensionContext
是否应执行给定的测试方法。
当注册多个ExecutionCondition
扩展时,只要其中一个条件返回disabled,容器或测试就会被禁用。因此,不能保证评估条件,因为另一个扩展可能已经导致容器或测试被禁用。换句话说,评估的工作原理类似于短路布尔 OR 运算符。
有关具体示例,请参阅DisabledCondition
和@Disabled
的源代码。
5.3.1. 禁用条件
有时,在没有某些条件处于活动状态的情况下运行测试套件可能很有用。例如,您可能希望即使测试使用@Disabled
注释也运行测试,以查看它们是否仍然损坏。为此,请为junit.jupiter.conditions.deactivate
配置参数提供一个模式,以指定应为当前测试运行禁用的(即不评估的)条件。该模式可以作为 JVM 系统属性提供,作为传递给Launcher
的LauncherDiscoveryRequest
中的配置参数,或者通过 JUnit Platform 配置文件提供(有关详细信息,请参阅配置参数)。
例如,要禁用 JUnit 的@Disabled
条件,可以使用以下系统属性启动 JVM。
-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition
模式匹配语法
有关详细信息,请参阅模式匹配语法。
5.4. 测试实例预构造回调
TestInstancePreConstructCallback
定义了希望在构造测试实例(通过构造函数调用或通过TestInstanceFactory
)之前调用的Extensions
的 API。
此扩展提供了对TestInstancePreDestroyCallback
的对称调用,并且与其他扩展结合使用时非常有用,可以准备构造函数参数或跟踪测试实例及其生命周期。
5.5. 测试实例工厂
TestInstanceFactory
定义了希望创建测试类实例的Extensions
的 API。
常见用例包括从依赖注入框架获取测试实例或调用静态工厂方法来创建测试类实例。
如果没有注册TestInstanceFactory
,框架将调用测试类的唯一构造函数来实例化它,可能通过注册的ParameterResolver
扩展解析构造函数参数。
实现 TestInstanceFactory
的扩展可以在测试接口、顶级测试类或 @Nested
测试类上注册。
为任何单个类注册多个实现 |
5.6. 测试实例后处理
TestInstancePostProcessor
定义了 Extensions
的 API,这些 Extensions
希望后处理测试实例。
常见用例包括将依赖项注入测试实例、在测试实例上调用自定义初始化方法等。
有关具体示例,请参阅 MockitoExtension
和 SpringExtension
的源代码。
5.7. 测试实例预销毁回调
TestInstancePreDestroyCallback
定义了 Extensions
的 API,这些 Extensions
希望在测试实例在测试中使用后且在销毁之前处理测试实例。
常见用例包括清理已注入测试实例的依赖项、在测试实例上调用自定义反初始化方法等。
5.8. 参数解析
ParameterResolver
定义了 Extension
的 API,用于在运行时动态解析参数。
如果测试类构造函数、测试方法或生命周期方法(参见 定义)声明了一个参数,则该参数必须在运行时由 ParameterResolver
进行解析。ParameterResolver
可以是内置的(参见 TestInfoParameterResolver
)或由用户注册。一般来说,参数可以通过名称、类型、注释或它们的任何组合来解析。
如果您希望实现一个自定义 ParameterResolver
,该自定义 ParameterResolver
仅根据参数的类型解析参数,您可能会发现扩展 TypeBasedParameterResolver
很方便,它充当此类用例的通用适配器。
有关具体示例,请参阅 CustomTypeParameterResolver
、CustomAnnotationParameterResolver
和 MapOfListsTypeBasedParameterResolver
的源代码。
由于 JDK 9 之前的 JDK 版本上的 因此,提供给
|
其他扩展也可以利用注册的 |
5.9. 测试结果处理
TestWatcher
定义了 Extensions
的 API,这些 Extensions
希望处理测试方法执行的结果。具体来说,TestWatcher
将使用以下事件的上下文信息进行调用。
-
testDisabled
:在禁用的测试方法被跳过之后调用 -
testSuccessful
:在测试方法成功完成之后调用 -
testAborted
:在测试方法被中止之后调用 -
testFailed
:在测试方法失败之后调用
与 定义 中介绍的“测试方法”定义相反,在此上下文中,测试方法是指任何 @Test 方法或 @TestTemplate 方法(例如,@RepeatedTest 或 @ParameterizedTest )。 |
实现此接口的扩展可以在类级别、实例级别或方法级别注册。当在类级别注册时,TestWatcher
将为任何包含的测试方法(包括 @Nested
类中的那些方法)调用。当在方法级别注册时,TestWatcher
将仅为注册了它的测试方法调用。
如果 为了确保 |
如果在类级别发生错误(例如,@BeforeAll
方法抛出的异常),则不会报告任何测试结果。类似地,如果测试类通过 ExecutionCondition
被禁用(例如,@Disabled
),则不会报告任何测试结果。
与其他 Extension API 相反,TestWatcher
不允许对测试执行产生负面影响。因此,TestWatcher
API 中的方法抛出的任何异常都将在 WARNING
级别记录,并且不允许传播或导致测试执行失败。
在 |
5.10. 测试生命周期回调
以下接口定义了在测试执行生命周期的各个点扩展测试的 API。有关示例,请参阅以下部分,并参阅 org.junit.jupiter.api.extension
包中每个接口的 Javadoc 以获取更多详细信息。
实现多个扩展 API 扩展开发人员可以选择在一个扩展中实现任意数量的这些接口。有关具体示例,请参阅 SpringExtension 的源代码。 |
5.10.1. 测试执行前后回调
BeforeTestExecutionCallback
和 AfterTestExecutionCallback
定义了 Extensions
的 API,这些 Extensions
希望添加将在测试方法执行之前和之后立即执行的行为。因此,这些回调非常适合计时、跟踪和类似用例。如果您需要实现围绕 @BeforeEach
和 @AfterEach
方法调用的回调,请改为实现 BeforeEachCallback
和 AfterEachCallback
。
以下示例展示了如何使用这些回调来计算和记录测试方法的执行时间。TimingExtension
同时实现了 BeforeTestExecutionCallback
和 AfterTestExecutionCallback
,以便计时和记录测试执行。
import java.lang.reflect.Method;
import java.util.logging.Logger;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final Logger logger = Logger.getLogger(TimingExtension.class.getName());
private static final String START_TIME = "start time";
@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
getStore(context).put(START_TIME, System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
Method testMethod = context.getRequiredTestMethod();
long startTime = getStore(context).remove(START_TIME, long.class);
long duration = System.currentTimeMillis() - startTime;
logger.info(() ->
String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
}
private Store getStore(ExtensionContext context) {
return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
}
}
由于 TimingExtensionTests
类通过 @ExtendWith
注册了 TimingExtension
,因此它的测试将在执行时应用此计时。
@ExtendWith(TimingExtension.class)
class TimingExtensionTests {
@Test
void sleep20ms() throws Exception {
Thread.sleep(20);
}
@Test
void sleep50ms() throws Exception {
Thread.sleep(50);
}
}
以下是运行 TimingExtensionTests
时产生的日志示例。
INFO: Method [sleep20ms] took 24 ms. INFO: Method [sleep50ms] took 53 ms.
5.11. 异常处理
在测试执行期间抛出的异常可能会被拦截并相应地处理,然后再进一步传播,以便可以在专门的 Extensions
中定义某些操作,例如错误日志记录或资源释放。JUnit Jupiter 提供了 Extensions
的 API,这些 Extensions
希望通过 TestExecutionExceptionHandler
处理在 @Test
方法期间抛出的异常,以及通过 LifecycleMethodExecutionExceptionHandler
处理在测试生命周期方法(@BeforeAll
、@BeforeEach
、@AfterEach
和 @AfterAll
)期间抛出的异常。
以下示例展示了一个扩展,它将吞并所有 IOException
实例,但会重新抛出任何其他类型的异常。
public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
throws Throwable {
if (throwable instanceof IOException) {
return;
}
throw throwable;
}
}
另一个示例展示了如何在设置和清理期间抛出意外异常时,准确记录被测应用程序的状态。请注意,与依赖生命周期回调(可能根据测试状态执行或不执行)不同,此解决方案保证在 @BeforeAll
、@BeforeEach
、@AfterEach
或 @AfterAll
失败后立即执行。
class RecordStateOnErrorExtension implements LifecycleMethodExecutionExceptionHandler {
@Override
public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during class setup");
throw ex;
}
@Override
public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during test setup");
throw ex;
}
@Override
public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during test cleanup");
throw ex;
}
@Override
public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during class cleanup");
throw ex;
}
}
可以按声明顺序为同一个生命周期方法调用多个执行异常处理程序。如果其中一个处理程序吞并了处理的异常,则后续处理程序将不会执行,并且不会将任何错误传播到 JUnit 引擎,就好像从未抛出异常一样。处理程序也可以选择重新抛出异常或抛出不同的异常,可能包装原始异常。
实现 LifecycleMethodExecutionExceptionHandler
的扩展,希望处理在 @BeforeAll
或 @AfterAll
期间抛出的异常,需要在类级别注册,而 BeforeEach
和 AfterEach
的处理程序也可以为单个测试方法注册。
// Register handlers for @Test, @BeforeEach, @AfterEach as well as @BeforeAll and @AfterAll
@ExtendWith(ThirdExecutedHandler.class)
class MultipleHandlersTestCase {
// Register handlers for @Test, @BeforeEach, @AfterEach only
@ExtendWith(SecondExecutedHandler.class)
@ExtendWith(FirstExecutedHandler.class)
@Test
void testMethod() {
}
}
5.12. 拦截调用
InvocationInterceptor
定义了 Extensions
的 API,这些 Extensions
希望拦截对测试代码的调用。
以下示例展示了一个扩展,它在 Swing 的事件调度线程中执行所有测试方法。
public class SwingEdtInterceptor implements InvocationInterceptor {
@Override
public void interceptTestMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext extensionContext) throws Throwable {
AtomicReference<Throwable> throwable = new AtomicReference<>();
SwingUtilities.invokeAndWait(() -> {
try {
invocation.proceed();
}
catch (Throwable t) {
throwable.set(t);
}
});
Throwable t = throwable.get();
if (t != null) {
throw t;
}
}
}
5.13. 为测试模板提供调用上下文
一个 @TestTemplate
方法只有在注册了至少一个 TestTemplateInvocationContextProvider
时才能执行。每个这样的提供者负责提供一个 Stream
的 TestTemplateInvocationContext
实例。每个上下文可以指定一个自定义的显示名称和一个额外的扩展列表,这些扩展只会在下一次调用 @TestTemplate
方法时使用。
以下示例展示了如何编写测试模板以及如何注册和实现 TestTemplateInvocationContextProvider
。
final List<String> fruits = Arrays.asList("apple", "banana", "lemon");
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String fruit) {
assertTrue(fruits.contains(fruit));
}
public class MyTestTemplateInvocationContextProvider
implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext context) {
return Stream.of(invocationContext("apple"), invocationContext("banana"));
}
private TestTemplateInvocationContext invocationContext(String parameter) {
return new TestTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return parameter;
}
@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new ParameterResolver() {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(String.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameter;
}
});
}
};
}
}
在这个例子中,测试模板将被调用两次。调用的显示名称将是 apple
和 banana
,如调用上下文所指定。每次调用都会注册一个自定义的 ParameterResolver
,它用于解析方法参数。使用 ConsoleLauncher
时,输出如下。
└─ testTemplate(String) ✔ ├─ apple ✔ └─ banana ✔
TestTemplateInvocationContextProvider
扩展 API 主要用于实现不同类型的测试,这些测试依赖于对类似测试的方法的重复调用,尽管是在不同的上下文中——例如,使用不同的参数,通过以不同的方式准备测试类实例,或者多次调用而不修改上下文。请参考 重复测试 或 参数化测试 的实现,它们使用此扩展点来提供其功能。
5.14. 在扩展中保持状态
通常,扩展只实例化一次。所以问题就变成了:如何将扩展的一次调用中的状态保持到下一次调用?ExtensionContext
API 提供了一个 Store
,专门用于此目的。扩展可以将值放入存储中,以便以后检索。请参阅 TimingExtension
,它展示了如何使用 Store
来实现方法级作用域。重要的是要记住,在测试执行期间存储在 ExtensionContext
中的值在周围的 ExtensionContext
中将不可用。由于 ExtensionContexts
可能嵌套,内部上下文的范围也可能受到限制。有关通过 Store
存储和检索值的可用方法的详细信息,请参阅相应的 Javadoc。
ExtensionContext.Store.CloseableResource CloseableResource 的实例,都会通过调用其 close() 方法来通知它们,通知顺序与它们添加时的顺序相反。 |
5.15. 扩展中支持的实用程序
junit-platform-commons
工件公开了一个名为 org.junit.platform.commons.support
的包,其中包含用于处理注释、类、反射和类路径扫描任务的维护的实用程序方法。TestEngine
和 Extension
作者鼓励使用这些支持的方法,以与 JUnit Platform 的行为保持一致。
5.15.1. 注释支持
AnnotationSupport
提供了对带注释元素(例如,包、注释、类、接口、构造函数、方法和字段)进行操作的静态实用程序方法。这些方法包括检查元素是否使用特定注释或元注释进行注释、搜索特定注释以及在类或接口中查找带注释的方法和字段。其中一些方法会在已实现的接口和类层次结构中搜索以查找注释。有关更多详细信息,请参阅 AnnotationSupport
的 Javadoc。
5.15.2. 类支持
ClassSupport
提供了用于处理类(即 java.lang.Class
的实例)的静态实用程序方法。有关更多详细信息,请参阅 ClassSupport
的 Javadoc。
5.15.3. 反射支持
ReflectionSupport
提供了增强标准 JDK 反射和类加载机制的静态实用程序方法。这些方法包括扫描类路径以搜索匹配指定谓词的类、加载和创建类的新的实例以及查找和调用方法。其中一些方法会遍历类层次结构以查找匹配的方法。有关更多详细信息,请参阅 ReflectionSupport
的 Javadoc。
5.15.4. 修饰符支持
ModifierSupport
提供了用于处理成员和类修饰符的静态实用程序方法——例如,确定成员是否声明为 public
、private
、abstract
、static
等。有关更多详细信息,请参阅 ModifierSupport
的 Javadoc。
5.16. 用户代码和扩展的相对执行顺序
当执行包含一个或多个测试方法的测试类时,除了用户提供的测试和生命周期方法之外,还会调用许多扩展回调。
另请参阅:测试执行顺序 |
5.16.1. 用户代码和扩展代码
下图说明了用户提供的代码和扩展代码的相对顺序。用户提供的测试和生命周期方法以橙色显示,扩展实现的回调代码以蓝色显示。灰色框表示单个测试方法的执行,并且将针对测试类中的每个测试方法重复执行。
下表进一步解释了 用户代码和扩展代码 图中的十六个步骤。
步骤 | 接口/注释 | 描述 |
---|---|---|
1 |
接口 |
在执行容器的所有测试之前执行的扩展代码 |
2 |
注释 |
在执行容器的所有测试之前执行的用户代码 |
3 |
接口 |
用于处理从 |
4 |
接口 |
在执行每个测试之前执行的扩展代码 |
5 |
注释 |
在执行每个测试之前执行的用户代码 |
6 |
接口 |
用于处理从 |
7 |
接口 |
在执行测试之前立即执行的扩展代码 |
8 |
注释 |
实际测试方法的用户代码 |
9 |
接口 |
用于处理测试期间抛出的异常的扩展代码 |
10 |
接口 |
在测试执行及其相应的异常处理程序之后立即执行的扩展代码 |
11 |
注释 |
在执行每个测试之后执行的用户代码 |
12 |
接口 |
用于处理从 |
13 |
接口 |
在执行每个测试之后执行的扩展代码 |
14 |
注释 |
在执行容器的所有测试之后执行的用户代码 |
15 |
接口 |
用于处理从 |
16 |
接口 |
在执行容器的所有测试之后执行的扩展代码 |
在最简单的情况下,只会执行实际的测试方法(步骤 8);所有其他步骤都是可选的,具体取决于是否存在用户代码或扩展对相应生命周期回调的支持。有关各种生命周期回调的更多详细信息,请参阅每个注释和扩展的相应 Javadoc。
上表中所有用户代码方法的调用都可以通过实现 InvocationInterceptor
来拦截。
5.16.2. 回调的包装行为
JUnit Jupiter 始终保证对注册的多个实现生命周期回调的扩展(例如 BeforeAllCallback
、AfterAllCallback
、BeforeEachCallback
、AfterEachCallback
、BeforeTestExecutionCallback
和 AfterTestExecutionCallback
)的包装行为。
这意味着,给定两个扩展 Extension1
和 Extension2
,其中 Extension1
在 Extension2
之前注册,则保证 Extension1
实现的任何“before”回调都将在 Extension2
实现的任何“before”回调之前执行。类似地,给定这两个相同扩展,以相同的顺序注册,则保证 Extension1
实现的任何“after”回调都将在 Extension2
实现的任何“after”回调之后执行。因此,Extension1
被称为包装 Extension2
。
JUnit Jupiter 还保证用户提供的生命周期方法(请参阅 定义)在类和接口层次结构中的包装行为。
-
@BeforeAll
方法从超类继承,只要它们没有被隐藏、覆盖或取代(即,仅根据签名替换,与 Java 的可见性规则无关)。此外,超类中的@BeforeAll
方法将在子类中的@BeforeAll
方法之前执行。-
类似地,在接口中声明的
@BeforeAll
方法将被继承,只要它们没有被隐藏或覆盖,并且接口中的@BeforeAll
方法将在实现该接口的类中的@BeforeAll
方法之前执行。
-
-
@AfterAll
方法从超类继承,只要它们没有被隐藏、覆盖或取代(即,仅根据签名替换,与 Java 的可见性规则无关)。此外,超类中的@AfterAll
方法将在子类中的@AfterAll
方法之后执行。-
类似地,在接口中声明的
@AfterAll
方法将被继承,只要它们没有被隐藏或覆盖,并且接口中的@AfterAll
方法将在实现该接口的类中的@AfterAll
方法之后执行。
-
-
@BeforeEach
方法从超类继承,只要它们没有被覆盖或取代(即,仅根据签名替换,与 Java 的可见性规则无关)。此外,超类中的@BeforeEach
方法将在子类中的@BeforeEach
方法之前执行。-
类似地,声明为接口默认方法的
@BeforeEach
方法将被继承,只要它们没有被覆盖,并且默认方法将在实现该接口的类中的@BeforeEach
方法之前执行。
-
-
@AfterEach
方法从超类继承,只要它们没有被覆盖或取代(即,仅根据签名替换,与 Java 的可见性规则无关)。此外,超类中的@AfterEach
方法将在子类中的@AfterEach
方法之后执行。-
类似地,声明为接口默认方法的
@AfterEach
方法将被继承,只要它们没有被覆盖,并且默认方法将在实现该接口的类中的@AfterEach
方法之后执行。
-
以下示例演示了这种行为。请注意,这些示例实际上并没有做任何实际的事情。相反,它们模拟了用于测试与数据库交互的常见场景。从Logger
类静态导入的所有方法都记录上下文信息,以便帮助我们更好地理解用户提供的回调方法和扩展中的回调方法的执行顺序。
import static example.callbacks.Logger.afterEachCallback;
import static example.callbacks.Logger.beforeEachCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class Extension1 implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
beforeEachCallback(this);
}
@Override
public void afterEach(ExtensionContext context) {
afterEachCallback(this);
}
}
import static example.callbacks.Logger.afterEachCallback;
import static example.callbacks.Logger.beforeEachCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
public class Extension2 implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
beforeEachCallback(this);
}
@Override
public void afterEach(ExtensionContext context) {
afterEachCallback(this);
}
}
import static example.callbacks.Logger.afterAllMethod;
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeAllMethod;
import static example.callbacks.Logger.beforeEachMethod;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
/**
* Abstract base class for tests that use the database.
*/
abstract class AbstractDatabaseTests {
@BeforeAll
static void createDatabase() {
beforeAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".createDatabase()");
}
@BeforeEach
void connectToDatabase() {
beforeEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".connectToDatabase()");
}
@AfterEach
void disconnectFromDatabase() {
afterEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".disconnectFromDatabase()");
}
@AfterAll
static void destroyDatabase() {
afterAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".destroyDatabase()");
}
}
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeAllMethod;
import static example.callbacks.Logger.beforeEachMethod;
import static example.callbacks.Logger.testMethod;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Extension of {@link AbstractDatabaseTests} that inserts test data
* into the database (after the database connection has been opened)
* and deletes test data (before the database connection is closed).
*/
@ExtendWith({ Extension1.class, Extension2.class })
class DatabaseTestsDemo extends AbstractDatabaseTests {
@BeforeAll
static void beforeAll() {
beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".beforeAll()");
}
@BeforeEach
void insertTestDataIntoDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
}
@Test
void testDatabaseFunctionality() {
testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
}
@AfterEach
void deleteTestDataFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
}
@AfterAll
static void afterAll() {
beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".afterAll()");
}
}
当执行DatabaseTestsDemo
测试类时,将记录以下内容。
@BeforeAll AbstractDatabaseTests.createDatabase() @BeforeAll DatabaseTestsDemo.beforeAll() Extension1.beforeEach() Extension2.beforeEach() @BeforeEach AbstractDatabaseTests.connectToDatabase() @BeforeEach DatabaseTestsDemo.insertTestDataIntoDatabase() @Test DatabaseTestsDemo.testDatabaseFunctionality() @AfterEach DatabaseTestsDemo.deleteTestDataFromDatabase() @AfterEach AbstractDatabaseTests.disconnectFromDatabase() Extension2.afterEach() Extension1.afterEach() @BeforeAll DatabaseTestsDemo.afterAll() @AfterAll AbstractDatabaseTests.destroyDatabase()
以下时序图有助于进一步阐明当执行DatabaseTestsDemo
测试类时,JupiterTestEngine
内部实际发生了什么。
JUnit Jupiter **不**保证在单个测试类或测试接口中声明的多个生命周期方法的执行顺序。有时可能看起来 JUnit Jupiter 按字母顺序调用这些方法。但是,这并不完全正确。排序类似于单个测试类中@Test
方法的排序。
在单个测试类或测试接口中声明的生命周期方法将使用确定性但有意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行生命周期方法,从而允许可重复的构建。 |
此外,JUnit Jupiter **不支持**在单个测试类或测试接口中声明的多个生命周期方法的包装行为。
以下示例演示了这种行为。具体来说,生命周期方法配置由于本地声明的生命周期方法的执行顺序而损坏。
-
测试数据在打开数据库连接之前插入,这会导致无法连接到数据库。
-
数据库连接在删除测试数据之前关闭,这会导致无法连接到数据库。
import static example.callbacks.Logger.afterEachMethod;
import static example.callbacks.Logger.beforeEachMethod;
import static example.callbacks.Logger.testMethod;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* Example of "broken" lifecycle method configuration.
*
* <p>Test data is inserted before the database connection has been opened.
*
* <p>Database connection is closed before deleting test data.
*/
@ExtendWith({ Extension1.class, Extension2.class })
class BrokenLifecycleMethodConfigDemo {
@BeforeEach
void connectToDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".connectToDatabase()");
}
@BeforeEach
void insertTestDataIntoDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
}
@Test
void testDatabaseFunctionality() {
testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
}
@AfterEach
void deleteTestDataFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
}
@AfterEach
void disconnectFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".disconnectFromDatabase()");
}
}
当执行BrokenLifecycleMethodConfigDemo
测试类时,将记录以下内容。
Extension1.beforeEach() Extension2.beforeEach() @BeforeEach BrokenLifecycleMethodConfigDemo.insertTestDataIntoDatabase() @BeforeEach BrokenLifecycleMethodConfigDemo.connectToDatabase() @Test BrokenLifecycleMethodConfigDemo.testDatabaseFunctionality() @AfterEach BrokenLifecycleMethodConfigDemo.disconnectFromDatabase() @AfterEach BrokenLifecycleMethodConfigDemo.deleteTestDataFromDatabase() Extension2.afterEach() Extension1.afterEach()
以下时序图有助于进一步阐明当执行BrokenLifecycleMethodConfigDemo
测试类时,JupiterTestEngine
内部实际发生了什么。
由于上述行为,JUnit 团队建议开发人员在每个测试类或测试接口中最多声明一种类型的生命周期方法(参见定义),除非这些生命周期方法之间没有依赖关系。 |
6. 高级主题
6.1. JUnit 平台报告
junit-platform-reporting
工件包含TestExecutionListener
实现,这些实现以两种形式生成 XML 测试报告:传统 和 Open Test Reporting。
该模块还包含其他TestExecutionListener 实现,可用于构建自定义报告。有关详细信息,请参见使用监听器和拦截器。 |
6.1.1. 传统 XML 格式
LegacyXmlReportGeneratingListener
为TestPlan
中的每个根生成一个单独的 XML 报告。请注意,生成的 XML 格式与基于 JUnit 4 的测试报告的事实标准兼容,该标准因 Ant 构建系统而流行。
LegacyXmlReportGeneratingListener
也被控制台启动器 使用。
6.1.2. Open Test Reporting XML 格式
OpenTestReportGeneratingListener
以Open Test Reporting 指定的基于事件的格式为整个执行写入 XML 报告,该格式支持 JUnit 平台的所有功能,例如分层测试结构、显示名称、标签等。
该监听器会自动注册,并且可以通过以下配置参数 进行配置
junit.platform.reporting.open.xml.enabled=true|false
-
启用/禁用写入报告。
junit.platform.reporting.output.dir=<path>
-
配置报告的输出目录。默认情况下,如果找到 Gradle 构建脚本,则使用
build
,如果找到 Maven POM,则使用target
;否则,使用当前工作目录。
如果启用,该监听器将在配置的输出目录中为每次测试运行创建一个名为junit-platform-events-<random-id>.xml
的 XML 报告文件。
可以使用Open Test Reporting CLI 工具 从基于事件的格式转换为分层格式,该格式更易于人类阅读。 |
Gradle
对于 Gradle,可以通过系统属性启用和配置写入与 Open Test Reporting 兼容的 XML 报告。以下示例将其输出目录配置为与 Gradle 用于其自身 XML 报告的目录相同。CommandLineArgumentProvider
用于使任务在不同机器之间可重定位,这在使用 Gradle 的构建缓存时很重要。
dependencies {
testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.10.2")
}
tasks.withType(Test).configureEach {
def outputDir = reports.junitXml.outputLocation
jvmArgumentProviders << ({
[
"-Djunit.platform.reporting.open.xml.enabled=true",
"-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}"
]
} as CommandLineArgumentProvider)
}
dependencies {
testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.10.2")
}
tasks.withType<Test>().configureEach {
val outputDir = reports.junitXml.outputLocation
jvmArgumentProviders += CommandLineArgumentProvider {
listOf(
"-Djunit.platform.reporting.open.xml.enabled=true",
"-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}"
)
}
}
Maven
对于 Maven Surefire/Failsafe,您可以启用 Open Test Reporting 输出,并将生成的 XML 文件配置为写入 Surefire/Failsafe 用于其自身 XML 报告的相同目录,如下所示
<project>
<!-- ... -->
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-reporting</artifactId>
<version>1.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<properties>
<configurationParameters>
junit.platform.reporting.open.xml.enabled = true
junit.platform.reporting.output.dir = target/surefire-reports
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
</project>
控制台启动器
使用控制台启动器 时,可以通过--config
设置配置参数来启用 Open Test Reporting 输出
$ java -jar junit-platform-console-standalone-1.10.2.jar <OPTIONS> \
--config=junit.platform.reporting.open.xml.enabled=true \
--config=junit.platform.reporting.output.dir=reports
6.2. JUnit 平台套件引擎
JUnit 平台支持使用 JUnit 平台从任何测试引擎声明式定义和执行测试套件。
6.2.1. 设置
除了junit-platform-suite-api
和junit-platform-suite-engine
工件之外,您还需要类路径上的至少一个其他测试引擎及其依赖项。有关组 ID、工件 ID 和版本的详细信息,请参见依赖项元数据。
6.2.2. @Suite 示例
通过使用@Suite
注释类,它被标记为 JUnit 平台上的测试套件。如以下示例所示,然后可以使用选择器和过滤器注释来控制套件的内容。
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
@IncludeClassNamePatterns(".*Tests")
class SuiteDemo {
}
其他配置选项 在测试套件中发现和过滤测试有许多配置选项。有关支持的注释的完整列表和更多详细信息,请参阅org.junit.platform.suite.api 包的 Javadoc。 |
6.3. JUnit 平台测试工具包
junit-platform-testkit
工件提供了在 JUnit 平台上执行测试计划并验证预期结果的支持。从 JUnit 平台 1.4 开始,此支持仅限于执行单个TestEngine
(参见引擎测试工具包)。
6.3.1. 引擎测试工具包
org.junit.platform.testkit.engine
包提供了在 JUnit 平台上执行给定TestPlan
的支持,然后通过流畅的 API 访问结果以验证预期结果。此 API 的关键入口点是EngineTestKit
,它提供名为engine()
和execute()
的静态工厂方法。建议您选择其中一个engine()
变体以从用于构建LauncherDiscoveryRequest
的流畅 API 中获益。
如果您希望使用Launcher API 中的LauncherDiscoveryRequestBuilder 来构建您的LauncherDiscoveryRequest ,则必须在EngineTestKit 中使用其中一个execute() 变体。 |
以下使用 JUnit Jupiter 编写的测试类将在后续示例中使用。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import example.util.Calculator;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
@TestMethodOrder(OrderAnnotation.class)
public class ExampleTestCase {
private final Calculator calculator = new Calculator();
@Test
@Disabled("for demonstration purposes")
@Order(1)
void skippedTest() {
// skipped ...
}
@Test
@Order(2)
void succeedingTest() {
assertEquals(42, calculator.multiply(6, 7));
}
@Test
@Order(3)
void abortedTest() {
assumeTrue("abc".contains("Z"), "abc does not contain Z");
// aborted ...
}
@Test
@Order(4)
void failingTest() {
// The following throws an ArithmeticException: "/ by zero"
calculator.divide(1, 0);
}
}
为了简洁起见,以下部分演示了如何测试 JUnit 自身的JupiterTestEngine
,其唯一的引擎 ID 为"junit-jupiter"
。如果您想测试自己的TestEngine
实现,则需要使用其唯一的引擎 ID。或者,您可以通过向EngineTestKit.engine(TestEngine)
静态工厂方法提供其实例来测试自己的TestEngine
。
6.3.2. 断言统计信息
测试工具包最常见的特性之一是能够对在执行TestPlan
期间触发的事件断言统计信息。以下测试演示了如何对 JUnit JupiterTestEngine
中的容器和测试断言统计信息。有关可用统计信息的详细信息,请参阅EventStatistics
的 Javadoc。
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
class EngineTestKitStatisticsDemo {
@Test
void verifyJupiterContainerStats() {
EngineTestKit
.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.containerEvents() (4)
.assertStatistics(stats -> stats.started(2).succeeded(2)); (5)
}
@Test
void verifyJupiterTestStats() {
EngineTestKit
.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.testEvents() (6)
.assertStatistics(stats ->
stats.skipped(1).started(3).succeeded(1).aborted(1).failed(1)); (7)
}
}
1 | 选择 JUnit JupiterTestEngine 。 |
2 | 选择ExampleTestCase 测试类。 |
3 | 执行TestPlan 。 |
4 | 按容器事件过滤。 |
5 | 断言容器事件的统计信息。 |
6 | 按测试事件过滤。 |
7 | 断言测试事件的统计信息。 |
在verifyJupiterContainerStats() 测试方法中,started 和succeeded 统计信息的计数为2 ,因为JupiterTestEngine 和ExampleTestCase 类都被视为容器。 |
6.3.3. 断言事件
例如,如果您想验证ExampleTestCase
中的skippedTest()
方法被跳过的原因,您可以按如下方式执行。
以下示例中的 有关可用于对事件进行 AssertJ 断言的条件的详细信息,请参阅 |
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.test;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.platform.testkit.engine.Events;
class EngineTestKitSkippedMethodDemo {
@Test
void verifyJupiterMethodWasSkipped() {
String methodName = "skippedTest";
Events testEvents = EngineTestKit (5)
.engine("junit-jupiter") (1)
.selectors(selectMethod(ExampleTestCase.class, methodName)) (2)
.execute() (3)
.testEvents(); (4)
testEvents.assertStatistics(stats -> stats.skipped(1)); (6)
testEvents.assertThatEvents() (7)
.haveExactly(1, event(test(methodName),
skippedWithReason("for demonstration purposes")));
}
}
1 | 选择 JUnit JupiterTestEngine 。 |
2 | 选择ExampleTestCase 测试类中的skippedTest() 方法。 |
3 | 执行TestPlan 。 |
4 | 按测试事件过滤。 |
5 | 将测试Events 保存到本地变量中。 |
6 | 可选地断言预期统计信息。 |
7 | 断言记录的测试事件包含一个名为skippedTest 的跳过测试,其原因为"for demonstration purposes" 。 |
如果您想验证从ExampleTestCase
中的failingTest()
方法抛出的异常类型,您可以按如下方式执行。
有关可用于针对事件和执行结果的 AssertJ 断言的条件的详细信息,请分别查阅 |
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
class EngineTestKitFailedMethodDemo {
@Test
void verifyJupiterMethodFailed() {
EngineTestKit.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.testEvents() (4)
.assertThatEvents().haveExactly(1, (5)
event(test("failingTest"),
finishedWithFailure(
instanceOf(ArithmeticException.class), message("/ by zero"))));
}
}
1 | 选择 JUnit JupiterTestEngine 。 |
2 | 选择ExampleTestCase 测试类。 |
3 | 执行TestPlan 。 |
4 | 按测试事件过滤。 |
5 | 断言记录的测试事件包含一个名为 failingTest 的失败测试,该测试的异常类型为 ArithmeticException ,错误消息为 "/ by zero" 。 |
虽然通常不需要,但有时您需要验证在执行 TestPlan
期间触发的所有事件。以下测试演示了如何通过 EngineTestKit
API 中的 assertEventsMatchExactly()
方法来实现这一点。
由于 |
如果您想进行部分匹配,有或没有排序要求,您可以分别使用 assertEventsMatchLooselyInOrder()
和 assertEventsMatchLoosely()
方法。
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.container;
import static org.junit.platform.testkit.engine.EventConditions.engine;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.started;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import java.io.StringWriter;
import java.io.Writer;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.opentest4j.TestAbortedException;
class EngineTestKitAllEventsDemo {
@Test
void verifyAllJupiterEvents() {
Writer writer = // create a java.io.Writer for debug output
EngineTestKit.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.allEvents() (4)
.debug(writer) (5)
.assertEventsMatchExactly( (6)
event(engine(), started()),
event(container(ExampleTestCase.class), started()),
event(test("skippedTest"), skippedWithReason("for demonstration purposes")),
event(test("succeedingTest"), started()),
event(test("succeedingTest"), finishedSuccessfully()),
event(test("abortedTest"), started()),
event(test("abortedTest"),
abortedWithReason(instanceOf(TestAbortedException.class),
message(m -> m.contains("abc does not contain Z")))),
event(test("failingTest"), started()),
event(test("failingTest"), finishedWithFailure(
instanceOf(ArithmeticException.class), message("/ by zero"))),
event(container(ExampleTestCase.class), finishedSuccessfully()),
event(engine(), finishedSuccessfully()));
}
}
1 | 选择 JUnit JupiterTestEngine 。 |
2 | 选择ExampleTestCase 测试类。 |
3 | 执行TestPlan 。 |
4 | 按所有事件过滤。 |
5 | 将所有事件打印到提供的 writer 中,以供调试。调试信息也可以写入 OutputStream ,例如 System.out 或 System.err 。 |
6 | 断言所有事件,严格按照测试引擎触发的顺序。 |
前面的示例中的 debug()
调用产生的输出类似于以下内容。
All Events:
Event [type = STARTED, testDescriptor = JupiterEngineDescriptor: [engine:junit-jupiter], timestamp = 2018-12-14T12:45:14.082280Z, payload = null]
Event [type = STARTED, testDescriptor = ClassTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase], timestamp = 2018-12-14T12:45:14.089339Z, payload = null]
Event [type = SKIPPED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:skippedTest()], timestamp = 2018-12-14T12:45:14.094314Z, payload = 'for demonstration purposes']
Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:succeedingTest()], timestamp = 2018-12-14T12:45:14.095182Z, payload = null]
Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:succeedingTest()], timestamp = 2018-12-14T12:45:14.104922Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]
Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:abortedTest()], timestamp = 2018-12-14T12:45:14.106121Z, payload = null]
Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:abortedTest()], timestamp = 2018-12-14T12:45:14.109956Z, payload = TestExecutionResult [status = ABORTED, throwable = org.opentest4j.TestAbortedException: Assumption failed: abc does not contain Z]]
Event [type = STARTED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:failingTest()], timestamp = 2018-12-14T12:45:14.110680Z, payload = null]
Event [type = FINISHED, testDescriptor = TestMethodTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase]/[method:failingTest()], timestamp = 2018-12-14T12:45:14.111217Z, payload = TestExecutionResult [status = FAILED, throwable = java.lang.ArithmeticException: / by zero]]
Event [type = FINISHED, testDescriptor = ClassTestDescriptor: [engine:junit-jupiter]/[class:example.ExampleTestCase], timestamp = 2018-12-14T12:45:14.113731Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]
Event [type = FINISHED, testDescriptor = JupiterEngineDescriptor: [engine:junit-jupiter], timestamp = 2018-12-14T12:45:14.113806Z, payload = TestExecutionResult [status = SUCCESSFUL, throwable = null]]
6.4. JUnit Platform Launcher API
JUnit 5 的主要目标之一是使 JUnit 与其编程客户端(构建工具和 IDE)之间的接口更加强大和稳定。目的是将发现和执行测试的内部机制与从外部进行的所有过滤和配置分离。
JUnit 5 引入了 Launcher
的概念,可用于发现、过滤和执行测试。此外,第三方测试库(如 Spock、Cucumber 和 FitNesse)可以通过提供自定义的 TestEngine 来插入 JUnit Platform 的启动基础设施。
启动器 API 位于 junit-platform-launcher
模块中。
启动器 API 的一个示例使用者是 ConsoleLauncher
,它位于 junit-platform-console
项目中。
6.4.1. 发现测试
将测试发现作为平台本身的专用功能,使 IDE 和构建工具免于在 JUnit 的先前版本中识别测试类和测试方法所遇到的许多困难。
用法示例
import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.platform.engine.FilterResult;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.junit.platform.launcher.PostDiscoveryFilter;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherConfig;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.junit.platform.launcher.listeners.TestExecutionSummary;
import org.junit.platform.reporting.legacy.xml.LegacyXmlReportGeneratingListener;
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("com.example.mytests"),
selectClass(MyTestClass.class)
)
.filters(
includeClassNamePatterns(".*Tests")
)
.build();
try (LauncherSession session = LauncherFactory.openSession()) {
TestPlan testPlan = session.getLauncher().discover(request);
// ... discover additional test plans or execute tests
}
您可以选择类、方法和包中的所有类,甚至搜索类路径或模块路径中的所有测试。发现发生在所有参与的测试引擎中。
生成的 TestPlan
是所有符合 LauncherDiscoveryRequest
的引擎、类和测试方法的分层(只读)描述。客户端可以遍历树,检索有关节点的详细信息,并获取指向原始源(如类、方法或文件位置)的链接。测试计划中的每个节点都有一个唯一 ID,可用于调用特定测试或测试组。
客户端可以通过 LauncherDiscoveryRequestBuilder
注册一个或多个 LauncherDiscoveryListener
实现,以深入了解测试发现期间发生的事件。默认情况下,构建器注册一个“在失败时中止”监听器,该监听器在遇到第一个发现失败后中止测试发现。默认的 LauncherDiscoveryListener
可以通过 junit.platform.discovery.listener.default
配置参数 进行更改。
6.4.2. 执行测试
要执行测试,客户端可以使用与发现阶段相同的 LauncherDiscoveryRequest
,也可以创建新的请求。可以通过向 Launcher
注册一个或多个 TestExecutionListener
实现来实现测试进度和报告,如下例所示。
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(
selectPackage("com.example.mytests"),
selectClass(MyTestClass.class)
)
.filters(
includeClassNamePatterns(".*Tests")
)
.build();
SummaryGeneratingListener listener = new SummaryGeneratingListener();
try (LauncherSession session = LauncherFactory.openSession()) {
Launcher launcher = session.getLauncher();
// Register a listener of your choice
launcher.registerTestExecutionListeners(listener);
// Discover tests and build a test plan
TestPlan testPlan = launcher.discover(request);
// Execute test plan
launcher.execute(testPlan);
// Alternatively, execute the request directly
launcher.execute(request);
}
TestExecutionSummary summary = listener.getSummary();
// Do something with the summary...
execute()
方法没有返回值,但您可以使用 TestExecutionListener
来聚合结果。例如,请参阅 SummaryGeneratingListener
、LegacyXmlReportGeneratingListener
和 UniqueIdTrackingListener
。
所有 TestExecutionListener 方法都按顺序调用。开始事件的方法按注册顺序调用,而结束事件的方法按相反顺序调用。测试用例执行在所有 executionStarted 调用返回之前不会开始。 |
6.4.3. 注册 TestEngine
有关详细信息,请参阅有关 TestEngine 注册 的专用部分。
6.4.4. 注册 PostDiscoveryFilter
除了在传递给 Launcher
API 的 LauncherDiscoveryRequest
中指定后发现过滤器之外,PostDiscoveryFilter
实现将在运行时通过 Java 的 ServiceLoader
机制被发现,并由 Launcher
自动应用,除了那些作为请求的一部分的过滤器之外。
例如,在 /META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter
文件中声明的实现 PostDiscoveryFilter
的 example.CustomTagFilter
类将被加载并自动应用。
6.4.5. 注册 LauncherSessionListener
注册的 LauncherSessionListener
实现将在打开 LauncherSession
(在 Launcher
首次发现和执行测试之前)和关闭(当不再发现或执行测试时)时收到通知。它们可以通过传递给 LauncherFactory
的 LauncherConfig
以编程方式注册,或者它们可以通过 Java 的 ServiceLoader
机制在运行时被发现,并自动注册到 LauncherSession
(除非禁用自动注册)。
工具支持
以下构建工具和 IDE 据悉为 LauncherSession
提供了完全支持
-
Gradle 4.6 及更高版本
-
Maven Surefire/Failsafe 3.0.0-M6 及更高版本
-
IntelliJ IDEA 2017.3 及更高版本
其他工具也可能有效,但尚未经过明确测试。
用法示例
LauncherSessionListener
非常适合实现一次性 JVM 设置/拆卸行为,因为它分别在启动器会话中的第一个测试之前和最后一个测试之后调用。启动器会话的范围取决于使用的 IDE 或构建工具,但通常对应于测试 JVM 的生命周期。一个在执行第一个测试之前启动 HTTP 服务器并在最后一个测试执行完毕后停止它的自定义监听器可能如下所示
package example.session;
import static java.net.InetAddress.getLoopbackAddress;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import com.sun.net.httpserver.HttpServer;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;
public class GlobalSetupTeardownListener implements LauncherSessionListener {
private Fixture fixture;
@Override
public void launcherSessionOpened(LauncherSession session) {
// Avoid setup for test discovery by delaying it until tests are about to be executed
session.getLauncher().registerTestExecutionListeners(new TestExecutionListener() {
@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
if (fixture == null) {
fixture = new Fixture();
fixture.setUp();
}
}
});
}
@Override
public void launcherSessionClosed(LauncherSession session) {
if (fixture != null) {
fixture.tearDown();
fixture = null;
}
}
static class Fixture {
private HttpServer server;
private ExecutorService executorService;
void setUp() {
try {
server = HttpServer.create(new InetSocketAddress(getLoopbackAddress(), 0), 0);
}
catch (IOException e) {
throw new UncheckedIOException("Failed to start HTTP server", e);
}
server.createContext("/test", exchange -> {
exchange.sendResponseHeaders(204, -1);
exchange.close();
});
executorService = Executors.newCachedThreadPool();
server.setExecutor(executorService);
server.start(); (1)
int port = server.getAddress().getPort();
System.setProperty("http.server.host", getLoopbackAddress().getHostAddress()); (2)
System.setProperty("http.server.port", String.valueOf(port)); (3)
}
void tearDown() {
server.stop(0); (4)
executorService.shutdownNow();
}
}
}
1 | 启动 HTTP 服务器 |
2 | 将其主机地址导出为系统属性,供测试使用 |
3 | 将其端口导出为系统属性,供测试使用 |
4 | 停止 HTTP 服务器 |
此示例使用来自 JDK 的 jdk.httpserver 模块的 HTTP 服务器实现,但与任何其他服务器或资源的工作方式类似。为了使监听器被 JUnit Platform 拾取,您需要将其注册为服务,方法是在您的测试运行时类路径中添加一个具有以下名称和内容的资源文件(例如,通过将文件添加到 src/test/resources
中)
example.session.GlobalSetupTeardownListener
您现在可以使用测试中的资源
package example.session;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import org.junit.jupiter.api.Test;
class HttpTests {
@Test
void respondsWith204() throws Exception {
String host = System.getProperty("http.server.host"); (1)
String port = System.getProperty("http.server.port"); (2)
URL url = URI.create("http://" + host + ":" + port + "/test").toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
int responseCode = connection.getResponseCode(); (3)
assertEquals(204, responseCode); (4)
}
}
1 | 从监听器设置的系统属性中读取服务器的主机地址 |
2 | 从监听器设置的系统属性中读取服务器的端口 |
3 | 向服务器发送请求 |
4 | 检查响应的状态代码 |
6.4.6. 注册 LauncherInterceptor
为了拦截 Launcher
和 LauncherSessionListener
实例的创建以及对前者的 discover
和 execute
方法的调用,客户端可以通过 Java 的 ServiceLoader
机制注册 LauncherInterceptor
的自定义实现,方法是将 junit.platform.launcher.interceptors.enabled
配置参数 设置为 true
。
一个典型的用例是创建一个自定义的替换 JUnit Platform 用于加载测试类和引擎实现的 ClassLoader
。
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import org.junit.platform.launcher.LauncherInterceptor;
public class CustomLauncherInterceptor implements LauncherInterceptor {
private final URLClassLoader customClassLoader;
public CustomLauncherInterceptor() throws Exception {
ClassLoader parent = Thread.currentThread().getContextClassLoader();
customClassLoader = new URLClassLoader(new URL[] { URI.create("some.jar").toURL() }, parent);
}
@Override
public <T> T intercept(Invocation<T> invocation) {
Thread currentThread = Thread.currentThread();
ClassLoader originalClassLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(customClassLoader);
try {
return invocation.proceed();
}
finally {
currentThread.setContextClassLoader(originalClassLoader);
}
}
@Override
public void close() {
try {
customClassLoader.close();
}
catch (IOException e) {
throw new UncheckedIOException("Failed to close custom class loader", e);
}
}
}
6.4.7. 注册 LauncherDiscoveryListener
除了在 LauncherDiscoveryRequest
中指定发现监听器或通过 Launcher
API 以编程方式注册它们之外,自定义的 LauncherDiscoveryListener
实现可以通过 Java 的 ServiceLoader
机制在运行时被发现,并自动注册到通过 LauncherFactory
创建的 Launcher
。
例如,在 /META-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener
文件中声明的实现 LauncherDiscoveryListener
的 example.CustomLauncherDiscoveryListener
类将被加载并自动注册。
6.4.8. 注册 TestExecutionListener
除了用于以编程方式注册测试执行监听器的公共 Launcher
API 方法之外,自定义的 TestExecutionListener
实现将在运行时通过 Java 的 ServiceLoader
机制被发现,并自动注册到通过 LauncherFactory
创建的 Launcher
。
例如,实现 TestExecutionListener
并声明在 /META-INF/services/org.junit.platform.launcher.TestExecutionListener
文件中的 example.CustomTestExecutionListener
类将自动加载和注册。
6.4.9. 配置 TestExecutionListener
当 TestExecutionListener
通过 Launcher
API 以编程方式注册时,监听器可能提供以编程方式配置它的方法,例如,通过它的构造函数、setter 方法等。但是,当 TestExecutionListener
通过 Java 的 ServiceLoader
机制自动注册时(参见 注册 TestExecutionListener),用户无法直接配置监听器。在这种情况下,TestExecutionListener
的作者可以选择通过 配置参数 使监听器可配置。然后,监听器可以通过提供给 testPlanExecutionStarted(TestPlan)
和 testPlanExecutionFinished(TestPlan)
回调方法的 TestPlan
访问配置参数。参见 UniqueIdTrackingListener
以获取示例。
6.4.10. 禁用 TestExecutionListener
有时,在某些执行监听器处于活动状态的情况下运行测试套件可能很有用。例如,您可能有一个自定义的 TestExecutionListener
,它将测试结果发送到外部系统以进行报告,而在调试时,您可能不希望报告这些调试结果。为此,请为 junit.platform.execution.listeners.deactivate
配置参数提供一个模式,以指定应为当前测试运行禁用(即未注册)哪些执行监听器。
只有通过 |
模式匹配语法
有关详细信息,请参阅模式匹配语法。
6.4.11. 配置 Launcher
如果您需要对测试引擎和监听器的自动检测和注册进行细粒度控制,您可以创建 LauncherConfig
的实例,并将其提供给 LauncherFactory
。通常,LauncherConfig
的实例是通过内置的流畅构建器 API 创建的,如下面的示例所示。
LauncherConfig launcherConfig = LauncherConfig.builder()
.enableTestEngineAutoRegistration(false)
.enableLauncherSessionListenerAutoRegistration(false)
.enableLauncherDiscoveryListenerAutoRegistration(false)
.enablePostDiscoveryFilterAutoRegistration(false)
.enableTestExecutionListenerAutoRegistration(false)
.addTestEngines(new CustomTestEngine())
.addLauncherSessionListeners(new CustomLauncherSessionListener())
.addLauncherDiscoveryListeners(new CustomLauncherDiscoveryListener())
.addPostDiscoveryFilters(new CustomPostDiscoveryFilter())
.addTestExecutionListeners(new LegacyXmlReportGeneratingListener(reportsDir, out))
.addTestExecutionListeners(new CustomTestExecutionListener())
.build();
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectPackage("com.example.mytests"))
.build();
try (LauncherSession session = LauncherFactory.openSession(launcherConfig)) {
session.getLauncher().execute(request);
}
6.4.12. 干运行模式
通过 Launcher
API 运行测试时,您可以通过将 junit.platform.execution.dryRun.enabled
配置参数 设置为 true
来启用干运行模式。在这种模式下,Launcher
不会实际执行任何测试,但会通知注册的 TestExecutionListener
实例,就好像所有测试都被跳过并且它们的容器已成功一样。这对于测试构建配置中的更改或验证监听器是否按预期调用很有用,而无需等待所有测试执行完成。
6.5. 测试引擎
TestEngine
为特定编程模型的测试发现和执行提供便利。
6.5.1. JUnit 测试引擎
JUnit 提供了三个 TestEngine
实现。
-
junit-jupiter-engine
:JUnit Jupiter 的核心。 -
junit-vintage-engine
:JUnit 4 之上的一个薄层,允许使用 JUnit Platform 启动器基础设施运行传统测试(基于 JUnit 3.8 和 JUnit 4)。 -
junit-platform-suite-engine
:使用 JUnit Platform 启动器基础设施执行测试的声明式套件。
6.5.2. 自定义测试引擎
您可以通过实现 junit-platform-engine 模块中的接口并注册您的引擎来贡献您自己的自定义 TestEngine
。
每个 TestEngine
必须提供自己的唯一 ID、从 EngineDiscoveryRequest
中发现测试,并根据 ExecutionRequest
执行这些测试。
junit- 唯一 ID 前缀保留给 JUnit 团队的 TestEnginesJUnit Platform
|
为了在启动 JUnit Platform 之前促进 IDE 和工具中的测试发现,鼓励 TestEngine
实现使用 @Testable
注释。例如,JUnit Jupiter 中的 @Test
和 @TestFactory
注释使用 @Testable
进行元注释。有关更多详细信息,请参阅 @Testable
的 Javadoc。
如果您的自定义 TestEngine
需要配置,请考虑允许用户通过 配置参数 提供配置。但是请注意,强烈建议您为测试引擎支持的所有配置参数使用唯一的首字母缩略词。这样做将确保您的配置参数名称与其他测试引擎的配置参数名称之间没有冲突。此外,由于配置参数可以作为 JVM 系统属性提供,因此明智的做法是避免与其他系统属性的名称冲突。例如,JUnit Jupiter 使用 junit.jupiter.
作为其支持的所有配置参数的前缀。此外,与上面关于 TestEngine
ID 的 junit-
前缀的警告一样,您不应使用 junit.
作为您自己配置参数名称的前缀。
虽然目前还没有关于如何实现自定义 TestEngine
的官方指南,但您可以参考 JUnit 测试引擎 的实现或 JUnit 5 wiki 中列出的第三方测试引擎的实现。您还将在互联网上找到各种教程和博客,演示如何编写自定义 TestEngine
。
HierarchicalTestEngine 是 TestEngine SPI(由 junit-jupiter-engine 使用)的便捷抽象基实现,它只需要实现者提供测试发现的逻辑。它实现了 TestDescriptor 的执行,这些 TestDescriptor 实现 Node 接口,包括对并行执行的支持。 |
6.5.3. 注册 TestEngine
TestEngine
注册通过 Java 的 ServiceLoader
机制支持。
例如,junit-jupiter-engine
模块在 junit-jupiter-engine
JAR 中的 /META-INF/services
文件夹中名为 org.junit.platform.engine.TestEngine
的文件中注册其 org.junit.jupiter.engine.JupiterTestEngine
。
6.5.4. 要求
本节中的“必须”、“不得”、“需要”、“应”、“不应”、“应该”、“不应该”、“推荐”、“可以”和“可选”等词语应按 RFC 2119 中的描述进行解释。 |
强制性要求
为了与构建工具和 IDE 互操作,TestEngine
实现必须遵守以下要求
-
从
TestEngine.discover()
返回的TestDescriptor
必须是TestDescriptor
实例树的根。这意味着不得在节点及其后代之间存在任何循环。 -
TestEngine
必须能够为其先前生成的任何唯一 ID(从TestEngine.discover()
返回)发现UniqueIdSelectors
。这使得能够选择要执行或重新运行的测试子集。 -
传递给
TestEngine.execute()
的EngineExecutionListener
的executionSkipped
、executionStarted
和executionFinished
方法必须为从TestEngine.discover()
返回的树中的每个TestDescriptor
节点调用最多一次。父节点必须在子节点之前报告为已启动,并在子节点之后报告为已完成。如果节点报告为已跳过,则不得为其后代报告任何事件。
增强兼容性
遵守以下要求是可选的,但为了与构建工具和 IDE 增强兼容性,建议这样做
-
除非要指示空发现结果,否则从
TestEngine.discover()
返回的TestDescriptor
应该具有子节点,而不是完全动态的。这允许工具显示测试的结构并选择要执行的测试子集。 -
在解析
UniqueIdSelectors
时,TestEngine
应该只返回具有匹配唯一 ID(包括其祖先)的TestDescriptor
实例,但可以返回执行所选测试所需的额外同级或其他节点。 -
TestEngines
应该支持 标记 测试和容器,以便在发现测试时可以应用标记过滤器。
7. API 演变
JUnit 5 的主要目标之一是提高维护人员在 JUnit 被许多项目使用的情况下演变 JUnit 的能力。在 JUnit 4 中,最初作为内部构造添加的许多内容只被外部扩展编写者和工具构建者使用。这使得更改 JUnit 4 变得特别困难,有时甚至不可能。
这就是 JUnit 5 为所有公开可用的接口、类和方法引入定义的生命周期的原因。
7.1. API 版本和状态
每个发布的工件都有一个版本号 <major>.<minor>.<patch>
,所有公开可用的接口、类和方法都使用来自 @API 的 @API Guardian 项目进行注释。注释的 status
属性可以分配以下值之一。
状态 | 描述 |
---|---|
|
不得被除 JUnit 本身以外的任何代码使用。可能会在未经事先通知的情况下删除。 |
|
不应再使用;可能会在下一次次要版本中消失。 |
|
适用于新的实验性功能,我们希望获得反馈。 |
|
适用于在当前主要版本的下一个次要版本发布之前至少不会以向后不兼容的方式更改的功能。如果计划删除,它将首先降级为 |
|
适用于在当前主要版本( |
如果类型上存在@API
注释,则认为它也适用于该类型的所有公共成员。成员允许声明更低稳定性的不同status
值。
7.2. 实验性 API
下表列出了哪些 API 目前通过@API(status = EXPERIMENTAL)
被指定为实验性。在依赖此类 API 时应谨慎。
包名称 | 类型名称 | 自 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.3. 已弃用的 API
下表列出了哪些 API 目前通过@API(status = DEPRECATED)
被指定为已弃用。您应尽可能避免使用已弃用的 API,因为此类 API 可能会在即将发布的版本中删除。
包名称 | 类型名称 | 自 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.4. @API 工具支持
The @API Guardian 项目计划为使用 @API 注释的 API 的发布者和使用者提供工具支持。例如,工具支持可能会提供一种方法来检查是否根据@API
注释声明使用 JUnit API。
8. 贡献者
直接在 GitHub 上浏览 当前的贡献者列表。
9. 发布说明
发布说明可在此处获得 此处。
10. 附录
10.1. 可重现的构建
从 5.7 版本开始,JUnit 5 旨在使其非 javadoc JAR 成为 可重现的。
在相同的构建条件下,例如 Java 版本,重复构建应提供相同的逐字节输出。
这意味着任何人都可以重现 Maven Central/Sonatype 上工件的构建条件,并在本地生成相同的输出工件,从而确认存储库中的工件实际上是从该源代码生成的。
10.2. 依赖项元数据
最终发布版和里程碑版的工件部署到 Maven Central,快照工件部署到 Sonatype 的 快照存储库,位于 /org/junit 下。
10.2.1. JUnit Platform
-
组 ID:
org.junit.platform
-
版本:
1.10.2
-
工件 ID:
junit-platform-commons
-
JUnit Platform 的通用 API 和支持实用程序。任何使用
@API(status = INTERNAL)
注释的 API 仅用于 JUnit 框架本身。外部方对内部 API 的任何使用都不受支持! junit-platform-console
-
支持从控制台发现和执行 JUnit Platform 上的测试。有关详细信息,请参阅 控制台启动器。
junit-platform-console-standalone
-
包含所有依赖项的可执行 JAR 在 Maven Central 的 junit-platform-console-standalone 目录下提供。有关详细信息,请参阅 控制台启动器。
junit-platform-engine
-
测试引擎的公共 API。有关详细信息,请参阅 注册测试引擎。
junit-platform-jfr
-
为 JUnit Platform 上的 Java Flight Recorder 事件提供
LauncherDiscoveryListener
和TestExecutionListener
。有关详细信息,请参阅 Flight Recorder 支持。 junit-platform-launcher
-
用于配置和启动测试计划的公共 API——通常由 IDE 和构建工具使用。有关详细信息,请参阅 JUnit Platform 启动器 API。
junit-platform-reporting
-
生成测试报告的
TestExecutionListener
实现——通常由 IDE 和构建工具使用。有关详细信息,请参阅 JUnit Platform 报告。 junit-platform-runner
-
用于在 JUnit 4 环境中执行 JUnit Platform 上的测试和测试套件的运行器。有关详细信息,请参阅 使用 JUnit 4 运行 JUnit Platform。
junit-platform-suite
-
JUnit Platform Suite 工件,它传递依赖于
junit-platform-suite-api
和junit-platform-suite-engine
,以便在 Gradle 和 Maven 等构建工具中简化依赖项管理。 junit-platform-suite-api
-
用于在 JUnit Platform 上配置测试套件的注释。由 JUnit Platform Suite Engine 和 JUnitPlatform 运行器 支持。
junit-platform-suite-commons
-
在 JUnit Platform 上执行测试套件的通用支持实用程序。
junit-platform-suite-engine
-
在 JUnit Platform 上执行测试套件的引擎;仅在运行时需要。有关详细信息,请参阅 JUnit Platform Suite Engine。
junit-platform-testkit
-
提供支持,用于为给定的
TestEngine
执行测试计划,然后通过流畅的 API 访问结果以验证预期结果。
10.2.2. JUnit Jupiter
-
组 ID:
org.junit.jupiter
-
版本:
5.10.2
-
工件 ID:
junit-jupiter
-
JUnit Jupiter 聚合工件,它传递依赖于
junit-jupiter-api
、junit-jupiter-params
和junit-jupiter-engine
,以便在 Gradle 和 Maven 等构建工具中简化依赖项管理。 junit-jupiter-api
junit-jupiter-engine
-
JUnit Jupiter 测试引擎实现;仅在运行时需要。
junit-jupiter-params
-
支持 JUnit Jupiter 中的 参数化测试。
junit-jupiter-migrationsupport
-
支持从 JUnit 4 迁移到 JUnit Jupiter;仅在支持 JUnit 4 的
@Ignore
注释以及运行选定的 JUnit 4 规则时需要。
10.2.3. JUnit Vintage
-
组 ID:
org.junit.vintage
-
版本:
5.10.2
-
工件 ID:
junit-vintage-engine
-
JUnit Vintage 测试引擎实现,允许您在 JUnit Platform 上运行传统 JUnit 测试。传统测试包括使用 JUnit 3 或 JUnit 4 API 编写的测试,或者使用基于这些 API 的测试框架编写的测试。