1. 概述
本文档的目标是为编写测试的程序员、扩展作者和引擎作者以及构建工具和 IDE 供应商提供全面的参考文档。
本文档也以 PDF 下载 的形式提供。
1.1. 什么是 JUnit 5?
与之前的 JUnit 版本不同,JUnit 5 由来自三个不同子项目的多个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform 作为 在 JVM 上启动测试框架 的基础。它还定义了 TestEngine
API,用于开发在平台上运行的测试框架。此外,该平台还提供了一个 控制台启动器,用于从命令行启动平台,以及 JUnit Platform Suite Engine,用于使用平台上的一个或多个测试引擎运行自定义测试套件。流行的 IDE(参见 IntelliJ IDEA、Eclipse、NetBeans 和 Visual Studio Code)和构建工具(参见 Gradle、Maven 和 Ant)也对 JUnit Platform 提供了第一流的支持。
JUnit Jupiter 是 编程模型 和 扩展模型 的组合,用于在 JUnit 5 中编写测试和扩展。Jupiter 子项目提供了一个 TestEngine
,用于在平台上运行基于 Jupiter 的测试。
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
方法除外,该方法需要返回值)。
类和方法可见性
测试类、测试方法和生命周期方法不需要是 通常建议省略测试类、测试方法和生命周期方法的 |
字段和方法继承
测试类中的字段会被继承。例如,来自超类的 测试方法和生命周期方法会被继承,除非它们根据 Java 语言的可见性规则被覆盖。例如,来自超类的 另请参阅:字段和方法搜索语义 |
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() {
}
}
也可以使用 Java record
类作为测试类,如下例所示。
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
record MyFirstJUnitJupiterRecordTests() {
@Test
void addition() {
assertEquals(2, new Calculator().add(1, 1));
}
}
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
方法。
断言方法可以选择接受断言消息作为其第三个参数,该参数可以是 String
或 Supplier<String>
。
当使用 Supplier<String>
(例如,lambda 表达式)时,消息会被延迟评估。这可以提供性能优势,尤其是在消息构造复杂或耗时的情况下,因为它仅在断言失败时才会被评估。
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");
// Lazily evaluates generateFailureMessage('a','b').
assertTrue('a' < 'b', () -> generateFailureMessage('a','b'));
}
@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!";
}
private static String generateFailureMessage(char a, char b) {
return "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily." + (a < b);
}
}
使用
assertTimeoutPreemptively() 的抢占式超时
一个常见的例子是 Spring Framework 中的事务性测试支持。具体来说,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.assertInstanceOf
import org.junit.jupiter.api.assertNotNull
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)
}
}
@Test
fun `assertNotNull with a smart cast`() {
val nullablePerson: Person? = person
assertNotNull(nullablePerson)
// The compiler smart casts nullablePerson to a non-nullable object.
// The safe call operator (?.) isn't required.
assertEquals(person.firstName, nullablePerson.firstName)
assertEquals(person.lastName, nullablePerson.lastName)
}
@Test
fun `assertInstanceOf with a smart cast`() {
val maybePerson: Any = person
assertInstanceOf<Person>(maybePerson)
// The compiler smart casts maybePerson to a Person object,
// allowing to access the Person properties.
assertEquals(person.firstName, maybePerson.firstName)
assertEquals(person.lastName, maybePerson.lastName)
}
}
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. 假设
假设通常用于当继续执行给定测试没有意义时 — 例如,如果测试依赖于当前运行时环境中不存在的某些内容。
-
当假设有效时,假设方法不会抛出异常,并且测试的执行会像往常一样继续。
-
当假设无效时,假设方法会抛出
org.opentest4j.TestAbortedException
类型的异常,以表明测试应该中止而不是标记为失败。
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 4 的 org.junit.Assume 类中的方法进行假设。具体来说,JUnit Jupiter 支持 JUnit 4 的 AssumptionViolatedException 来表明测试应该中止而不是标记为失败。 |
2.7. 异常处理
JUnit Jupiter 提供了强大的异常处理支持。这包括用于管理由于异常导致的测试失败的内置机制、异常在实现断言和假设中的作用,以及如何专门断言代码中的非抛出条件。
2.7.1. 未捕获的异常
在 JUnit Jupiter 中,如果从测试方法、生命周期方法或扩展中抛出异常,并且该异常未在测试方法、生命周期方法或扩展中捕获,则框架会将测试或测试类标记为失败。
失败的假设偏离了这个一般规则。 与失败的断言相反,失败的假设不会导致测试失败;相反,失败的假设会导致测试中止。 有关更多详细信息和示例,请参阅 假设。 |
在以下示例中,failsDueToUncaughtException()
方法抛出一个 ArithmeticException
。由于异常未在测试方法中捕获,JUnit Jupiter 会将测试标记为失败。
private final Calculator calculator = new Calculator();
@Test
void failsDueToUncaughtException() {
// The following throws an ArithmeticException due to division by
// zero, which causes a test failure.
calculator.divide(1, 0);
}
重要的是要注意,在测试方法中指定 throws 子句对测试结果没有影响。JUnit Jupiter 不会将 throws 子句解释为关于测试方法应该抛出哪些异常的期望或断言。只有当异常被意外抛出或断言失败时,测试才会失败。 |
2.7.2. 失败的断言
JUnit Jupiter 中的断言是使用异常实现的。框架在 org.junit.jupiter.api.Assertions
类中提供了一组断言方法,当断言失败时,这些方法会抛出 AssertionError
。这种机制是 JUnit 如何处理作为异常的断言失败的核心方面。有关 JUnit Jupiter 断言支持的更多信息,请参阅 断言 部分。
第三方断言库可以选择抛出 AssertionError 来表示断言失败;但是,它们也可以选择抛出不同类型的异常来表示失败。另请参阅:第三方断言库。 |
JUnit Jupiter 本身不区分失败的断言 (AssertionError ) 和其他类型的异常。所有未捕获的异常都会导致测试失败。但是,集成开发环境 (IDE) 和其他工具可以通过检查抛出的异常是否是 AssertionError 的实例来区分这两种类型的失败。 |
在以下示例中,failsDueToUncaughtAssertionError()
方法抛出一个 AssertionError
。由于异常未在测试方法中捕获,JUnit Jupiter 会将测试标记为失败。
private final Calculator calculator = new Calculator();
@Test
void failsDueToUncaughtAssertionError() {
// The following incorrect assertion will cause a test failure.
// The expected value should be 2 instead of 99.
assertEquals(99, calculator.add(1, 1));
}
2.7.3. 断言预期的异常
JUnit Jupiter 提供了专门的断言,用于测试在预期条件下是否抛出了特定的异常。assertThrows()
和 assertThrowsExactly()
断言是验证您的代码是否通过抛出适当的异常来正确响应错误情况的关键工具。
使用 assertThrows()
assertThrows()
方法用于验证在提供的可执行代码块的执行期间是否抛出了特定类型的异常。它不仅检查抛出的异常的类型,还检查其子类,使其适用于更通用的异常处理测试。assertThrows()
断言方法返回抛出的异常对象,以便对其执行其他断言。
@Test
void testExpectedExceptionIsThrown() {
// The following assertion succeeds because the code under assertion
// throws the expected IllegalArgumentException.
// The assertion also returns the thrown exception which can be used for
// further assertions like asserting the exception message.
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("expected message");
});
assertEquals("expected message", exception.getMessage());
// The following assertion also succeeds because the code under assertion
// throws IllegalArgumentException which is a subclass of RuntimeException.
assertThrows(RuntimeException.class, () -> {
throw new IllegalArgumentException("expected message");
});
}
使用 assertThrowsExactly()
当您需要断言抛出的异常正是特定类型,而不允许预期异常类型的子类时,可以使用 assertThrowsExactly()
方法。当需要验证精确的异常处理行为时,这非常有用。与 assertThrows()
类似,assertThrowsExactly()
断言方法也返回抛出的异常对象,以便对其执行其他断言。
@Test
void testExpectedExceptionIsThrown() {
// The following assertion succeeds because the code under assertion throws
// IllegalArgumentException which is exactly equal to the expected type.
// The assertion also returns the thrown exception which can be used for
// further assertions like asserting the exception message.
IllegalArgumentException exception =
assertThrowsExactly(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("expected message");
});
assertEquals("expected message", exception.getMessage());
// The following assertion fails because the assertion expects exactly
// RuntimeException to be thrown, not subclasses of RuntimeException.
assertThrowsExactly(RuntimeException.class, () -> {
throw new IllegalArgumentException("expected message");
});
}
2.7.4. 断言没有预期的异常
尽管从测试方法抛出的任何异常都会导致测试失败,但在某些用例中,显式断言给定的代码块不抛出异常可能是有益的。当您想要验证特定的代码段是否未抛出任何异常时,可以使用 assertDoesNotThrow()
断言。
@Test
void testExceptionIsNotThrown() {
assertDoesNotThrow(() -> {
shouldNotThrowException();
});
}
void shouldNotThrowException() {
}
第三方断言库通常也提供类似的支持。例如,AssertJ 具有 assertThatNoException().isThrownBy(() → …) 。另请参阅:第三方断言库。 |
2.8. 禁用测试
整个测试类或单个测试方法可以通过 @Disabled
注解、条件测试执行 中讨论的注解之一或自定义 ExecutionCondition
来禁用。
当 @Disabled
应用于类级别时,该类中的所有测试方法也会自动禁用。
如果测试方法通过 @Disabled
禁用,则会阻止测试方法的执行和方法级别的生命周期回调,例如 @BeforeEach
方法、@AfterEach
方法和相应的扩展 API。但是,这不会阻止测试类被实例化,也不会阻止类级别的生命周期回调(例如 @BeforeAll
方法、@AfterAll
方法和相应的扩展 API)的执行。
这是一个 @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.9. 条件测试执行
JUnit Jupiter 中的 ExecutionCondition
扩展 API 允许开发人员基于某些条件以编程方式启用或禁用测试类或测试方法。这种条件最简单的示例是内置的 DisabledCondition
,它支持 @Disabled
注解(请参阅 禁用测试)。
除了 @Disabled
之外,JUnit Jupiter 还支持 org.junit.jupiter.api.condition
包中的其他几个基于注解的条件,这些条件允许开发人员声明式地启用或禁用测试类和测试方法。如果您希望提供有关它们可能被禁用的原因的详细信息,则与这些内置条件关联的每个注解都有一个可用于该目的的 disabledReason
属性。
当注册了多个 ExecutionCondition
扩展时,只要其中一个条件返回禁用,测试类或测试方法就会被禁用。如果测试类被禁用,则该类中的所有测试方法也会自动禁用。如果测试方法被禁用,则会阻止测试方法的执行和方法级别的生命周期回调,例如 @BeforeEach
方法、@AfterEach
方法和相应的扩展 API。但是,这不会阻止测试类被实例化,也不会阻止类级别的生命周期回调(例如 @BeforeAll
方法、@AfterAll
方法和相应的扩展 API)的执行。
有关详细信息,请参阅 ExecutionCondition
和以下部分。
组合注解
请注意,以下部分中列出的任何条件注解也可以用作元注解,以便创建自定义组合注解。例如,@EnabledOnOs 演示中的 |
JUnit Jupiter 中的条件注解不是 |
除非另有说明,否则以下部分中列出的每个条件注解只能在给定的测试接口、测试类或测试方法上声明一次。如果条件注解直接存在、间接存在或元存在于给定元素上多次,则只会使用 JUnit 发现的第一个此类注解;任何其他声明都将被静默忽略。但是请注意,每个条件注解都可以与 |
2.9.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.9.2. Java 运行时环境条件
可以通过 @EnabledOnJre
和 @DisabledOnJre
注解在特定版本的 Java 运行时环境 (JRE) 上启用或禁用容器或测试,或者通过 @EnabledForJreRange
和 @DisabledForJreRange
注解在 JRE 的特定版本范围内启用或禁用容器或测试。范围有效地默认为 JRE.JAVA_8
作为下限,JRE.OTHER
作为上限,这允许使用半开范围。
以下列表演示了如何将这些注解与预定义的 JRE 枚举常量一起使用。
@Test
@EnabledOnJre(JAVA_17)
void onlyOnJava17() {
// ...
}
@Test
@EnabledOnJre({ JAVA_17, JAVA_21 })
void onJava17And21() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9To11() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9)
void onJava9AndHigher() {
// ...
}
@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 notOnJava9AndHigher() {
// ...
}
@Test
@DisabledForJreRange(max = JAVA_11)
void notFromJava8To11() {
// ...
}
由于 JRE 中定义的枚举常量对于任何给定的 JUnit 版本都是静态的,因此您可能会发现需要配置 JRE
枚举不支持的 Java 版本。例如,截至 JUnit Jupiter 5.12,JRE
枚举定义 JAVA_25
为最高支持的 Java 版本。但是,您可能希望针对更高版本的 Java 运行测试。为了支持此类用例,您可以通过 @EnabledOnJre
和 @DisabledOnJre
中的 versions
属性以及 @EnabledForJreRange
和 @DisabledForJreRange
中的 minVersion
和 maxVersion
属性指定任意 Java 版本。
以下列表演示了如何将这些注解与任意 Java 版本一起使用。
@Test
@EnabledOnJre(versions = 26)
void onlyOnJava26() {
// ...
}
@Test
@EnabledOnJre(versions = { 25, 26 })
// Can also be expressed as follows.
// @EnabledOnJre(value = JAVA_25, versions = 26)
void onJava25And26() {
// ...
}
@Test
@EnabledForJreRange(minVersion = 26)
void onJava26AndHigher() {
// ...
}
@Test
@EnabledForJreRange(minVersion = 25, maxVersion = 27)
// Can also be expressed as follows.
// @EnabledForJreRange(min = JAVA_25, maxVersion = 27)
void fromJava25To27() {
// ...
}
@Test
@DisabledOnJre(versions = 26)
void notOnJava26() {
// ...
}
@Test
@DisabledOnJre(versions = { 25, 26 })
// Can also be expressed as follows.
// @DisabledOnJre(value = JAVA_25, versions = 26)
void notOnJava25And26() {
// ...
}
@Test
@DisabledForJreRange(minVersion = 26)
void notOnJava26AndHigher() {
// ...
}
@Test
@DisabledForJreRange(minVersion = 25, maxVersion = 27)
// Can also be expressed as follows.
// @DisabledForJreRange(min = JAVA_25, maxVersion = 27)
void notFromJava25To27() {
// ...
}
2.9.3. Native Image 条件
可以通过 @EnabledInNativeImage
和 @DisabledInNativeImage
注解在 GraalVM native image 中启用或禁用容器或测试。当使用来自 GraalVM Native Build Tools 项目的 Gradle 和 Maven 插件在 native image 中运行测试时,通常会使用这些注解。
@Test
@EnabledInNativeImage
void onlyWithinNativeImage() {
// ...
}
@Test
@DisabledInNativeImage
void neverWithinNativeImage() {
// ...
}
2.9.4. 系统属性条件
可以通过 @EnabledIfSystemProperty
和 @DisabledIfSystemProperty
注解基于 named
JVM 系统属性的值启用或禁用容器或测试。通过 matches
属性提供的值将被解释为正则表达式。
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
// ...
}
@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
// ...
}
从 JUnit Jupiter 5.6 开始, |
2.9.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.9.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.10. 标记和过滤
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
@Tag("model")
class TaggingDemo {
@Test
@Tag("taxes")
void testingTaxCalculation() {
}
}
有关演示如何为标签创建自定义注解的示例,请参阅 元注解和组合注解。 |
2.11. 测试执行顺序
默认情况下,测试类和方法将使用确定性但有意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行测试类和测试方法,从而允许可重复的构建。
有关测试方法和测试类的定义,请参阅 定义。 |
2.11.1. 方法顺序
虽然真正的单元测试通常不应依赖于它们的执行顺序,但在某些时候,强制执行特定的测试方法执行顺序是必要的 — 例如,在编写集成测试或功能测试时,测试的顺序很重要,尤其是在与 @TestInstance(Lifecycle.PER_CLASS)
结合使用时。
要控制测试方法的执行顺序,请使用 @TestMethodOrder
注解您的测试类或测试接口,并指定所需的 MethodOrderer
实现。您可以实现自己的自定义 MethodOrderer
,也可以使用以下内置 MethodOrderer
实现之一。
-
MethodOrderer.DisplayName
:根据测试方法的显示名称按字母数字顺序排序(请参阅 显示名称生成优先级规则) -
MethodOrderer.MethodName
:根据测试方法的名称和形式参数列表按字母数字顺序排序 -
MethodOrderer.OrderAnnotation
:根据通过@Order
注解指定的值按数字顺序排序测试方法 -
MethodOrderer.Random
:伪随机地排序测试方法,并支持自定义种子的配置 -
MethodOrderer.Alphanumeric
:根据测试方法的名称和形式参数列表按字母数字顺序排序;已弃用,推荐使用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.11.2. 类顺序
虽然测试类通常不应依赖于它们的执行顺序,但在某些时候,强制执行特定的测试类执行顺序是可取的。您可能希望以随机顺序执行测试类,以确保测试类之间没有意外的依赖关系,或者您可能希望对测试类进行排序以优化构建时间,如下列场景中所述。
-
首先运行先前失败的测试和更快的测试:“快速失败”模式
-
启用并行执行后,首先安排较长的测试:“最短测试计划执行持续时间”模式
-
各种其他用例
要为整个测试套件全局配置测试类执行顺序,请使用 junit.jupiter.testclass.order.default
配置参数 来指定您希望使用的 ClassOrderer
的完全限定类名。提供的类必须实现 ClassOrderer
接口。
您可以实现自己的自定义 ClassOrderer
,也可以使用以下内置 ClassOrderer
实现之一。
-
ClassOrderer.ClassName
:根据测试类的完全限定类名按字母数字顺序排序测试类 -
ClassOrderer.DisplayName
:根据测试类的显示名称按字母数字顺序排序(请参阅 显示名称生成优先级规则) -
ClassOrderer.OrderAnnotation
:根据通过@Order
注解指定的值按数字顺序排序测试类 -
ClassOrderer.Random
:伪随机地排序测试类,并支持自定义种子的配置
例如,为了使 @Order
注解在测试类上生效,您应该使用配置参数和相应的完全限定类名配置 ClassOrderer.OrderAnnotation
类排序器(例如,在 src/test/resources/junit-platform.properties
中)
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.12. 测试实例生命周期
为了允许在隔离状态下执行单个测试方法,并避免由于可变测试实例状态而导致的意外副作用,JUnit 在执行每个测试方法之前创建一个新的测试类实例(请参阅 定义)。这种“按方法”的测试实例生命周期是 JUnit Jupiter 中的默认行为,并且类似于所有以前版本的 JUnit。
请注意,即使“按方法”的测试实例生命周期模式处于活动状态,如果给定的测试方法通过 条件(例如,@Disabled 、@DisabledOnOs 等)禁用,测试类仍然会被实例化。 |
如果您希望 JUnit Jupiter 在同一测试实例上执行所有测试方法,请使用 @TestInstance(Lifecycle.PER_CLASS)
注解您的测试类。使用此模式时,每个测试类将创建一个新的测试实例。因此,如果您的测试方法依赖于存储在实例变量中的状态,您可能需要在 @BeforeEach
或 @AfterEach
方法中重置该状态。
“按类”模式比默认的“按方法”模式有一些额外的优势。具体来说,使用“按类”模式,可以在非静态方法以及接口 default
方法上声明 @BeforeAll
和 @AfterAll
。“按类”模式因此也使得可以在 @Nested
测试类中使用 @BeforeAll
和 @AfterAll
方法。
从 Java 16 开始,@BeforeAll 和 @AfterAll 方法可以在 @Nested 测试类中声明为 static 。 |
如果您正在使用 Kotlin 编程语言编写测试,您可能还会发现在切换到“按类”测试实例生命周期模式后,更容易实现非静态 @BeforeAll
和 @AfterAll
生命周期方法以及 @MethodSource
工厂方法。
2.12.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.13. 嵌套测试
@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 或更高版本,则可以将 @BeforeAll 和 @AfterAll 方法声明为 @Nested 测试类中的 static 方法,并且此限制不再适用。 |
2.14. 构造函数和方法的依赖注入
在所有之前的 JUnit 版本中,不允许测试构造函数或方法具有参数(至少在使用标准 Runner
实现时不允许)。作为 JUnit Jupiter 的主要更改之一,现在允许测试构造函数和方法都具有参数。这提供了更大的灵活性,并为构造函数和方法启用了依赖注入。
ParameterResolver
定义了测试扩展的 API,这些扩展希望在运行时动态解析参数。如果测试类构造函数、测试方法或生命周期方法(请参阅 定义)接受参数,则该参数必须在运行时由注册的 ParameterResolver
解析。
目前有三个内置的解析器会自动注册。
-
TestInfoParameterResolver
:如果构造函数或方法参数的类型为TestInfo
,则TestInfoParameterResolver
将提供与当前容器或测试相对应的TestInfo
实例作为参数的值。然后,TestInfo
可用于检索有关当前容器或测试的信息,例如显示名称、测试类、测试方法和关联的标签。显示名称可以是技术名称,例如测试类或测试方法的名称,也可以是通过@DisplayName
配置的自定义名称。TestInfo
充当 JUnit 4 中TestName
规则的直接替代品。以下演示了如何将TestInfo
注入到@BeforeAll
方法、测试类构造函数、@BeforeEach
方法和@Test
方法中。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeAll;
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 {
@BeforeAll
static void beforeAll(TestInfo testInfo) {
assertEquals("TestInfo Demo", testInfo.getDisplayName());
}
TestInfoDemo(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}
@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()
或fileEntryPublished()
方法使用。这允许它们在 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);
}
@Test
void reportFiles(TestReporter testReporter, @TempDir Path tempDir) throws Exception {
testReporter.publishFile("test1.txt", MediaType.TEXT_PLAIN_UTF_8,
file -> Files.write(file, singletonList("Test 1")));
Path existingFile = Files.write(tempDir.resolve("test2.txt"), singletonList("Test 2"));
testReporter.publishFile(existingFile, MediaType.TEXT_PLAIN_UTF_8);
testReporter.publishDirectory("test3", dir -> {
Files.write(dir.resolve("nested1.txt"), singletonList("Nested content 1"));
Files.write(dir.resolve("nested2.txt"), singletonList("Nested content 2"));
});
Path existingDir = Files.createDirectory(tempDir.resolve("test4"));
Files.write(existingDir.resolve("nested1.txt"), singletonList("Nested content 1"));
Files.write(existingDir.resolve("nested2.txt"), singletonList("Nested content 2"));
testReporter.publishDirectory(existingDir);
}
}
其他参数解析器必须通过使用 @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.15. 测试接口和默认方法
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();
assertNotEquals(null, value);
}
@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.16. 重复测试
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.16.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.17. 参数化测试
参数化测试使您可以使用不同的参数多次运行测试。它们的声明方式与常规 @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.17.1. 必需的设置
为了使用参数化测试,您需要在 junit-jupiter-params
工件上添加依赖项。有关详细信息,请参阅 依赖项元数据。
2.17.2. 使用参数
参数化测试方法通常直接从配置的源使用参数(请参阅 参数源),遵循参数源索引和方法参数索引之间的一对一关联(请参阅 @CsvSource 中的示例)。但是,参数化测试方法也可以选择将来自源的参数聚合到传递给方法的单个对象中(请参阅 参数聚合)。其他参数也可以由 ParameterResolver
提供(例如,获取 TestInfo
、TestReporter
等的实例)。具体来说,参数化测试方法必须根据以下规则声明形式参数。
-
必须首先声明零个或多个索引参数。
-
必须接下来声明零个或多个聚合器。
-
必须最后声明由
ParameterResolver
提供的零个或多个参数。
在此上下文中,索引参数是由 ArgumentsProvider
提供的 Arguments
中给定索引的参数,该参数作为参数传递给参数化方法的正式参数列表中的相同索引处。聚合器是 ArgumentsAccessor
类型的任何参数或使用 @AggregateWith
注解的任何参数。
AutoCloseable 参数
实现 为了防止这种情况发生,请在 |
2.17.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
和空值给我们的参数化测试可能很有用。以下注解充当接受单个参数的参数化测试的 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));
}
除了 names
之外,您还可以使用 from
和 to
属性来指定常量范围。该范围从 from
属性中指定的常量开始,并包括所有后续常量,直到并包括 to
属性中指定的常量,基于枚举常量的自然顺序。
如果省略 from
和 to
属性,则它们分别默认为枚举类型中的第一个和最后一个常量。如果省略所有 names
、from
和 to
属性,则将使用所有常量。以下示例演示了如何指定常量范围。
@ParameterizedTest
@EnumSource(from = "HOURS", to = "DAYS")
void testWithEnumSourceRange(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.HOURS, ChronoUnit.HALF_DAYS, ChronoUnit.DAYS).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"));
}
您还可以将 mode
与 from
、to
和 names
属性组合使用,以定义常量范围,同时从该范围中排除特定值,如下所示。
@ParameterizedTest
@EnumSource(from = "HOURS", to = "DAYS", mode = EXCLUDE, names = { "HALF_DAYS" })
void testWithEnumSourceRangeExclude(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.HOURS, ChronoUnit.DAYS).contains(unit));
assertFalse(EnumSet.of(ChronoUnit.HALF_DAYS).contains(unit));
}
@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;
}
}
@FieldSource
@FieldSource
允许您引用测试类或外部类的一个或多个字段。
测试类中的字段必须是 static
的,除非测试类使用 @TestInstance(Lifecycle.PER_CLASS)
注解;而外部类中的字段必须始终是 static
的。
每个字段都必须能够提供参数流,并且“流”中的每组“参数”都将作为带注解的 @ParameterizedTest
方法的各个调用的物理参数提供。
在此上下文中,“流”是 JUnit 可以可靠地转换为 Stream
的任何内容;但是,实际的具体字段类型可以采用多种形式。一般来说,这转化为 Collection
、Iterable
、流的 Supplier
(Stream
、DoubleStream
、LongStream
或 IntStream
)、Iterator
的 Supplier
、对象数组或原始数组。“流”中的每组“参数”可以作为 Arguments
的实例、对象数组(例如,Object[]
、String[]
等)提供,或者如果参数化测试方法接受单个参数,则可以作为单个值提供。
与 |
请注意,作为一组“参数”提供的对象的一维数组将以不同于其他类型的参数的方式处理。具体来说,对象的一维数组的所有元素都将作为单独的物理参数传递给 @ParameterizedTest
方法。有关更多详细信息,请参阅 @FieldSource
的 Javadoc。
如果您没有通过 @FieldSource
显式提供字段名称,则 JUnit Jupiter 将按照约定在测试类中搜索与当前 @ParameterizedTest
方法同名的字段。以下示例对此进行了演示。此参数化测试方法将被调用两次:值分别为 "apple"
和 "banana"
。
@ParameterizedTest
@FieldSource
void arrayOfFruits(String fruit) {
assertFruit(fruit);
}
static final String[] arrayOfFruits = { "apple", "banana" };
以下示例演示了如何通过 @FieldSource
提供单个显式字段名称。此参数化测试方法将被调用两次:值分别为 "apple"
和 "banana"
。
@ParameterizedTest
@FieldSource("listOfFruits")
void singleFieldSource(String fruit) {
assertFruit(fruit);
}
static final List<String> listOfFruits = Arrays.asList("apple", "banana");
以下示例演示了如何通过 @FieldSource
提供多个显式字段名称。此示例使用了上一个示例中的 listOfFruits
字段以及 additionalFruits
字段。因此,此参数化测试方法将被调用四次:值分别为 "apple"
、"banana"
、"cherry"
和 "dewberry"
。
@ParameterizedTest
@FieldSource({ "listOfFruits", "additionalFruits" })
void multipleFieldSources(String fruit) {
assertFruit(fruit);
}
static final Collection<String> additionalFruits = Arrays.asList("cherry", "dewberry");
只要流或迭代器包装在 java.util.function.Supplier
中,也可以通过 @FieldSource
字段提供 Stream
、DoubleStream
、IntStream
、LongStream
或 Iterator
作为参数源。以下示例演示了如何提供命名参数的 Stream
的 Supplier
。此参数化测试方法将被调用两次:值分别为 "apple"
和 "banana"
,显示名称分别为 Apple
和 Banana
。
@ParameterizedTest
@FieldSource
void namedArgumentsSupplier(String fruit) {
assertFruit(fruit);
}
static final Supplier<Stream<Arguments>> namedArgumentsSupplier = () -> Stream.of(
arguments(named("Apple", "apple")),
arguments(named("Banana", "banana"))
);
请注意, 同样, |
如果参数化测试方法声明了多个参数,则相应的 @FieldSource
字段必须能够提供 Arguments
实例或对象数组的集合、流供应商或数组,如下所示(有关支持的类型的更多详细信息,请参阅 @FieldSource
的 Javadoc)。
@ParameterizedTest
@FieldSource("stringIntAndListArguments")
void testWithMultiArgFieldSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static List<Arguments> stringIntAndListArguments = Arrays.asList(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
请注意, |
可以通过提供外部 static
@FieldSource
字段的完全限定字段名来引用它,如下例所示。
@ParameterizedTest
@FieldSource("example.FruitUtils#tropicalFruits")
void testWithExternalFieldSource(String tropicalFruit) {
// test with tropicalFruit
}
@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'
值。除非设置了 emptyValue
属性,否则空的带引号的值 (''
) 会产生一个空的 String
;而完全空的值则被解释为 null
引用。通过指定一个或多个 nullValues
,可以将自定义值解释为 null
引用(请参阅下表中的 NIL
示例)。如果 null
引用的目标类型是原始类型,则会抛出 ArgumentConversionException
。
无论通过 nullValues 属性配置了任何自定义值,未加引号的空值始终会转换为 null 引用。 |
默认情况下,除非在带引号的字符串中,否则 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"
值。除非设置了 emptyValue
属性,否则空的带引号的值 (""
) 会产生一个空的 String
;而完全空的值则被解释为 null
引用。通过指定一个或多个 nullValues
,可以将自定义值解释为 null
引用。如果 null
引用的目标类型是原始类型,则会抛出 ArgumentConversionException
。
无论通过 nullValues 属性配置了任何自定义值,未加引号的空值始终会转换为 null 引用。 |
默认情况下,除非在带引号的字符串中,否则 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
类。
此外,ArgumentsProvider
的实现可以声明构造函数参数,以防它们需要由注册的 ParameterResolver
解析,如下例所示。
public class MyArgumentsProviderWithConstructorInjection implements ArgumentsProvider {
private final TestInfo testInfo;
public MyArgumentsProviderWithConstructorInjection(TestInfo testInfo) {
this.testInfo = testInfo;
}
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(Arguments.of(testInfo.getDisplayName()));
}
}
使用可重复注解的多个来源
可重复注解提供了一种方便的方式来指定来自不同提供程序的多个来源。
@DisplayName("A parameterized test that makes use of repeatable annotations")
@ParameterizedTest
@MethodSource("someProvider")
@MethodSource("otherProvider")
void testWithRepeatedAnnotation(String argument) {
assertNotNull(argument);
}
static Stream<String> someProvider() {
return Stream.of("foo");
}
static Stream<String> otherProvider() {
return Stream.of("bar");
}
在上面的参数化测试之后,每个参数都将运行一个测试用例
[1] foo [2] bar
以下注解是可重复的
-
@ValueSource
-
@EnumSource
-
@MethodSource
-
@FieldSource
-
@CsvSource
-
@CsvFileSource
-
@ArgumentsSource
2.17.4. 参数计数验证
参数计数验证目前是一项实验性功能。欢迎您试用并向 JUnit 团队提供反馈,以便他们可以改进并最终推广此功能。 |
默认情况下,当参数来源提供的参数多于测试方法需要的参数时,这些额外的参数将被忽略,测试照常执行。这可能会导致错误,其中参数永远不会传递给参数化测试方法。
为了防止这种情况,您可以将参数计数验证设置为“strict”。然后,任何额外的参数都将导致错误。
要更改所有测试的此行为,请将 junit.jupiter.params.argumentCountValidation
配置参数 设置为 strict
。要更改单个测试的此行为,请使用 @ParameterizedTest
注解的 argumentCountValidation
属性
@ParameterizedTest(argumentCountValidation = ArgumentCountValidationMode.STRICT)
@CsvSource({ "42, -666" })
void testWithArgumentCountValidation(int number) {
assertTrue(number > 0);
}
2.17.5. 参数转换
拓宽转换
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 及其包装类。 |
目标类型 | 示例 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
回退 String 到 Object 的转换
除了从字符串到上表列出的目标类型的隐式转换之外,如果目标类型声明了恰好一个合适的工厂方法或工厂构造函数(如下定义),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;
}
}
显式转换
您可以显式指定一个 ArgumentConverter
用于某个参数,而不是依赖隐式参数转换,方法是使用 @ConvertWith
注解,如下例所示。请注意,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.17.6. 参数聚合
默认情况下,提供给 @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
接口,并通过 @AggregateWith
注解在 @ParameterizedTest
方法中的兼容参数上注册它。然后,聚合的结果将作为参数化测试被调用时相应参数的参数提供。请注意,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.17.7. 自定义显示名称
默认情况下,参数化测试调用的显示名称包含调用索引和该特定调用的所有参数的 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 ✔
请注意, |
自定义显示名称中支持以下占位符。
占位符 | 描述 |
---|---|
|
方法的显示名称 |
|
当前调用索引(从 1 开始) |
|
完整的、逗号分隔的参数列表 |
|
完整的、逗号分隔的参数列表,带有参数名称 |
|
参数集的名称 |
|
|
|
单个参数 |
当在显示名称中包含参数时,如果参数的字符串表示形式超过配置的最大长度,则会被截断。该限制可以通过 junit.jupiter.params.displayname.argument.maxlength 配置参数配置,默认值为 512 个字符。 |
当使用 @MethodSource
、@FieldSource
或 @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")))
);
}
使用 ConsoleLauncher
执行上述方法时,您将看到类似于以下内容的输出。
A parameterized test with named arguments ✔ ├─ 1: An important file ✔ └─ 2: Another file ✔
请注意, 同样, |
使用 ArgumentSet
API 为整组参数提供自定义名称,自定义名称将用作显示名称,如下例所示。
@DisplayName("A parameterized test with named argument sets")
@ParameterizedTest
@FieldSource("argumentSets")
void testWithArgumentSets(File file1, File file2) {
}
static List<Arguments> argumentSets = Arrays.asList(
argumentSet("Important files", new File("path1"), new File("path2")),
argumentSet("Other files", new File("path3"), new File("path4"))
);
使用 ConsoleLauncher
执行上述方法时,您将看到类似于以下内容的输出。
A parameterized test with named argument sets ✔ ├─ [1] Important files ✔ └─ [2] Other files ✔
请注意, |
如果您想为项目中的所有参数化测试设置默认名称模式,您可以在 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.17.8. 生命周期和互操作性
参数化测试的每次调用都具有与常规 @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.18. 测试模板
@TestTemplate
方法不是常规测试用例,而是测试用例的模板。因此,它旨在根据注册提供程序返回的调用上下文的数量多次调用。因此,它必须与注册的 TestTemplateInvocationContextProvider
扩展一起使用。测试模板方法的每次调用都像执行常规 @Test
方法一样,完全支持相同的生命周期回调和扩展。有关用法示例,请参阅为测试模板提供调用上下文。
2.19. 动态测试
在注解中描述的 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 方法生成的各个动态测试之间由回调方法或扩展重置。 |
2.19.1. 动态测试示例
以下 DynamicTestsDemo
类演示了测试工厂和动态测试的几个示例。
第一个方法返回无效的返回类型。由于无效的返回类型在编译时无法检测到,因此在运行时检测到时会抛出 JUnitException
。
接下来的六个方法演示了 DynamicTest
实例的 Collection
、Iterable
、Iterator
、数组或 Stream
的生成。这些示例大多数并没有真正展示动态行为,而只是原则上演示了支持的返回类型。但是,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 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.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, () -> assertEquals(0, n % 2)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTests() {
// Generates random positive integers between 0 and 100 until
// a number evenly divisible by 7 is encountered.
Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random();
int current;
@Override
public boolean hasNext() {
current = random.nextInt(100);
return current % 7 != 0;
}
@Override
public Integer next() {
return current;
}
};
// Generates display names like: input:5, input:37, input:85, etc.
Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
// Executes tests based on the current input value.
ThrowingConsumer<Integer> testExecutor = (input) -> 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<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.19.2. 动态测试和 Named
在某些情况下,使用 Named API 和 DynamicTest
上的相应 stream()
工厂方法来指定带有描述性名称的输入可能更自然,如下面的第一个示例所示。第二个示例更进一步,允许通过实现 Executable
接口以及通过 NamedExecutable
基类实现 Named
来提供应执行的代码块。
import static example.util.StringUtils.isPalindrome;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Named.named;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.NamedExecutable;
import org.junit.jupiter.api.TestFactory;
public class DynamicTestsNamedDemo {
@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
// Stream of palindromes to check
var 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<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNamedExecutables() {
// Stream of palindromes to check
var inputStream = Stream.of("racecar", "radar", "mom", "dad")
.map(PalindromeNamedExecutable::new);
// Returns a stream of dynamic tests based on NamedExecutables.
return DynamicTest.stream(inputStream);
}
record PalindromeNamedExecutable(String text) implements NamedExecutable {
@Override
public String getName() {
return String.format("'%s' is a palindrome", text);
}
@Override
public void execute() {
assertTrue(isPalindrome(text));
}
}
}
2.19.3. 动态测试的 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
的 Javadoc。 ClassSource
-
如果
URI
包含class
方案和完全限定类名——例如,class:org.junit.Foo?line=42
。 UriSource
-
如果以上
TestSource
实现均不适用。
2.20. 超时
@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.20.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.20.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.default
。
此类配置参数的值必须采用以下不区分大小写的格式:<数字> [ns|μs|ms|s|m|h|d]
。数字和单位之间的空格可以省略。如果未指定单位,则等同于使用秒。
参数值 | 等效注解 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.20.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.4. 调试超时
在对超时方法执行的线程调用 Thread.interrupt()
之前,会调用已注册的 Pre-Interrupt Callback 扩展。这允许检查应用程序状态并输出可能有助于诊断超时原因的附加信息。
超时时的线程转储
JUnit 注册了 Pre-Interrupt Callback 扩展点的默认实现,如果通过将 junit.jupiter.execution.timeout.threaddump.enabled
配置参数 设置为 true
启用,则会将所有线程的堆栈转储到 System.out
。
2.21. 并行执行
默认情况下,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
的测试类。在前一种情况下,测试作者必须确保测试类是线程安全的;在后一种情况下,并发执行可能会与配置的执行顺序冲突。因此,在这两种情况下,只有当测试类或方法上存在 @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
下图说明了对于 junit.jupiter.execution.parallel.mode.default
和 junit.jupiter.execution.parallel.mode.classes.default
的所有四种组合(请参阅第一列中的标签),每个类有两个测试方法的两个顶层测试类 A
和 B
的执行行为。
如果未显式设置 junit.jupiter.execution.parallel.mode.classes.default
配置参数,则将改用 junit.jupiter.execution.parallel.mode.default
的值。
2.21.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 将使用因子为 1
的 dynamic
配置策略。因此,所需的并行度将等于可用处理器/核心数。
并行度本身并不意味着最大并发线程数 默认情况下,JUnit Jupiter 不保证并发执行的测试数量不会超过配置的并行度。例如,当使用下一节中描述的同步机制之一时,幕后使用的 ForkJoinPool 可能会生成额外的线程,以确保执行继续具有足够的并行度。如果您需要此类保证,在 Java 9+ 中,可以通过控制 dynamic 、fixed 和 custom 策略的最大池大小来限制最大并发线程数。 |
相关属性
下表列出了用于配置并行执行的相关属性。有关如何设置此类属性的详细信息,请参阅 配置参数。
属性 | 描述 | 支持的值 | 默认值 |
---|---|---|---|
|
启用并行测试执行 |
|
|
|
测试树中节点的默认执行模式 |
|
|
|
顶层类的默认执行模式 |
|
|
|
所需并行度和最大池大小的执行策略 |
|
|
|
要乘以可用处理器/核心数以确定 |
正十进制数 |
|
|
要乘以可用处理器/核心数和 |
正十进制数,必须大于或等于 |
256 + |
|
禁用 |
|
|
|
|
正整数 |
无默认值 |
|
|
正整数,必须大于或等于 |
256 + |
|
禁用 |
|
|
|
要用于 |
例如,org.example.CustomStrategy |
无默认值 |
2.21.2. 同步
除了使用 @Execution
注解控制执行模式外,JUnit Jupiter 还提供了另一种基于注解的声明式同步机制。@ResourceLock
注解允许您声明测试类或方法使用特定的共享资源,该资源需要同步访问以确保可靠的测试执行。共享资源由唯一的名称标识,该名称是一个 String
。该名称可以是用户定义的,也可以是 Resources
中的预定义常量之一:SYSTEM_PROPERTIES
、SYSTEM_OUT
、SYSTEM_ERR
、LOCALE
或 TIME_ZONE
。
除了静态声明这些共享资源外,@ResourceLock
注解还具有 providers
属性,该属性允许注册 ResourceLocksProvider
接口的实现,该接口可以在运行时动态添加共享资源。请注意,使用 @ResourceLock
注解静态声明的资源与 ResourceLocksProvider
实现动态添加的资源相结合。
如果在不使用 @ResourceLock
的情况下并行运行以下示例中的测试,则它们将是不稳定的。有时它们会通过,有时它们会由于写入然后读取同一 JVM 系统属性的固有竞争条件而失败。
当使用 @ResourceLock
注解声明对共享资源的访问时,JUnit Jupiter 引擎会使用此信息来确保没有冲突的测试并行运行。此保证扩展到测试类或方法的生命周期方法。例如,如果测试方法使用 @ResourceLock
注解进行注解,则将在执行任何 @BeforeEach
方法之前获取“锁”,并在执行完所有 @AfterEach
方法之后释放“锁”。
隔离运行测试
如果大多数测试类可以在没有任何同步的情况下并行运行,但您有一些测试类需要隔离运行,则可以使用 |
除了唯一标识共享资源的 String
之外,您还可以指定访问模式。需要 READ
访问共享资源的两个测试可以彼此并行运行,但在任何其他需要 READ_WRITE
访问同一共享资源的测试运行时则不能并行运行。
@ResourceLock
注解“静态”声明共享资源@Execution(CONCURRENT)
class StaticSharedResourcesDemo {
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"));
}
}
ResourceLocksProvider
实现“动态”添加共享资源@Execution(CONCURRENT)
@ResourceLock(providers = DynamicSharedResourcesDemo.Provider.class)
class DynamicSharedResourcesDemo {
private Properties backup;
@BeforeEach
void backup() {
backup = new Properties();
backup.putAll(System.getProperties());
}
@AfterEach
void restore() {
System.setProperties(backup);
}
@Test
void customPropertyIsNotSetByDefault() {
assertNull(System.getProperty("my.prop"));
}
@Test
void canSetCustomPropertyToApple() {
System.setProperty("my.prop", "apple");
assertEquals("apple", System.getProperty("my.prop"));
}
@Test
void canSetCustomPropertyToBanana() {
System.setProperty("my.prop", "banana");
assertEquals("banana", System.getProperty("my.prop"));
}
static class Provider implements ResourceLocksProvider {
@Override
public Set<Lock> provideForMethod(List<Class<?>> enclosingInstanceTypes, Class<?> testClass,
Method testMethod) {
ResourceAccessMode mode = testMethod.getName().startsWith("canSet") ? READ_WRITE : READ;
return Collections.singleton(new Lock(SYSTEM_PROPERTIES, mode));
}
}
}
此外,也可以通过 @ResourceLock
注解中的 target
属性为直接子节点声明“静态”共享资源,该属性接受来自 ResourceLockTarget
枚举的值。
在类级别的 @ResourceLock
注解中指定 target = CHILDREN
具有与为此类中声明的每个测试方法和嵌套测试类添加具有相同 value
和 mode
的注解相同的语义。
当测试类声明 READ
锁,但只有少数方法持有 READ_WRITE
锁时,这可能会提高并行化。
如果在 @ResourceLock
没有 target = CHILDREN
的情况下,以下示例中的测试将在 SAME_THREAD
中运行。这是因为测试类声明了 READ
共享资源,但一个测试方法持有 READ_WRITE
锁,这将强制所有测试方法采用 SAME_THREAD
执行模式。
target
属性为子节点声明共享资源@Execution(CONCURRENT)
@ResourceLock(value = "a", mode = READ, target = CHILDREN)
public class ChildrenSharedResourcesDemo {
@ResourceLock(value = "a", mode = READ_WRITE)
@Test
void test1() throws InterruptedException {
Thread.sleep(2000L);
}
@Test
void test2() throws InterruptedException {
Thread.sleep(2000L);
}
@Test
void test3() throws InterruptedException {
Thread.sleep(2000L);
}
@Test
void test4() throws InterruptedException {
Thread.sleep(2000L);
}
@Test
void test5() throws InterruptedException {
Thread.sleep(2000L);
}
}
2.22. 内置扩展
虽然 JUnit 团队鼓励将可重用扩展打包并在单独的库中维护,但 JUnit Jupiter 包含了一些面向用户的扩展实现,这些实现被认为非常通用,用户无需添加其他依赖项。
2.22.1. @TempDir 扩展
内置的 TempDirectory
扩展用于为单个测试或测试类中的所有测试创建和清理临时目录。默认情况下已注册。要使用它,请使用 @TempDir
注解类型为 java.nio.file.Path
或 java.io.File
的非 final、未赋值字段,或者将使用 @TempDir
注解的类型为 java.nio.file.Path
或 java.io.File
的参数添加到测试类构造函数、生命周期方法或测试方法。
例如,以下测试为单个测试方法声明了一个使用 @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 。但是,请注意,此选项已弃用,将在未来版本中删除。 |
以下示例在 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 创建的。
以下示例定义了一个工厂,该工厂使用测试名称作为目录名称前缀,而不是 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
。
2.22.2. @AutoClose 扩展
内置的 AutoCloseExtension
自动关闭与字段关联的资源。默认情况下已注册。要使用它,请使用 @AutoClose
注解测试类中的字段。
@AutoClose
字段可以是 static
或非 static 的。如果在评估 @AutoClose
字段的值时该字段为 null
,则该字段将被忽略,但会记录警告消息以通知您。
默认情况下,@AutoClose
期望带注解的字段的值实现一个 close()
方法,该方法将被调用以关闭资源。但是,开发人员可以通过 value
属性自定义 close 方法的名称。例如,@AutoClose("shutdown")
指示 JUnit 查找 shutdown()
方法以关闭资源。
@AutoClose
字段从超类继承。此外,子类中的 @AutoClose
字段将在超类中的 @AutoClose
字段之前关闭。
当给定的测试类中存在多个 @AutoClose
字段时,资源关闭的顺序取决于确定性的但有意不明显的算法。这确保了测试套件的后续运行以相同的顺序关闭资源,从而允许可重复构建。
AutoCloseExtension
实现了 AfterAllCallback
和 TestInstancePreDestroyCallback
扩展 API。因此,static
@AutoClose
字段将在当前测试类中的所有测试完成后关闭,实际上是在为测试类执行 @AfterAll
方法之后。非 static 的 @AutoClose
字段将在销毁当前测试类实例之前关闭。具体而言,如果测试类配置了 @TestInstance(Lifecycle.PER_METHOD)
语义,则非 static 的 @AutoClose
字段将在执行每个测试方法、测试工厂方法或测试模板方法之后关闭。但是,如果测试类配置了 @TestInstance(Lifecycle.PER_CLASS)
语义,则非 static 的 @AutoClose
字段将不会关闭,直到不再需要当前测试类实例,这意味着在 @AfterAll
方法之后以及所有 static
@AutoClose
字段已关闭之后。
以下示例演示了如何使用 @AutoClose
注解实例字段,以便在测试执行后自动关闭资源。在此示例中,我们假设应用默认的 @TestInstance(Lifecycle.PER_METHOD)
语义。
@AutoClose
关闭资源的测试类class AutoCloseDemo {
@AutoClose (1)
WebClient webClient = new WebClient(); (2)
String serverUrl = // specify server URL ...
@Test
void getProductList() {
// Use WebClient to connect to web server and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
}
1 | 使用 @AutoClose 注解实例字段。 |
2 | WebClient 实现了 java.lang.AutoCloseable ,后者定义了一个 close() 方法,该方法将在每个 @Test 方法之后调用。 |
3. 从 JUnit 4 迁移
尽管 JUnit Jupiter 编程模型和扩展模型本身不支持 JUnit 4 的功能(如 Rules
和 Runners
),但源代码维护者无需更新其所有现有测试、测试扩展和自定义构建测试基础结构即可迁移到 JUnit Jupiter。
相反,JUnit 通过 JUnit Vintage 测试引擎 提供了一种平稳的迁移路径,该引擎允许使用 JUnit Platform 基础结构执行基于 JUnit 3 和 JUnit 4 的现有测试。由于特定于 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 Platform 启动器将自动拾取 JUnit 3 和 JUnit 4 测试。
请参阅 junit5-samples
存储库中的示例项目,以了解如何使用 Gradle 和 Maven 完成此操作。
3.2. 并行执行
JUnit Vintage 测试引擎支持并行执行顶层测试类和测试方法,从而允许现有 JUnit 3 和 JUnit 4 测试通过并发测试执行从改进的性能中受益。可以使用以下 配置参数 启用和配置它
junit.vintage.execution.parallel.enabled=true|false
-
启用/禁用并行执行(默认为
false
)。需要选择加入才能使用以下配置参数并行执行classes
或methods
。 junit.vintage.execution.parallel.classes=true|false
-
启用/禁用并行执行测试类(默认为
false
)。 junit.vintage.execution.parallel.methods=true|false
-
启用/禁用并行执行测试方法(默认为
false
)。 junit.vintage.execution.parallel.pool-size=<number>
-
指定用于并行执行的线程池的大小。默认情况下,使用可用处理器的数量。
junit-platform.properties
中的配置示例
junit.vintage.execution.parallel.enabled=true
junit.vintage.execution.parallel.classes=true
junit.vintage.execution.parallel.methods=true
junit.vintage.execution.parallel.pool-size=4
设置这些属性后,VintageTestEngine
将并行执行测试,从而可能显着减少整体测试套件执行时间。
3.3. 迁移技巧
以下是将现有 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 Rule 支持。
-
-
@Test(expected = …)
和ExpectedException
rule 不再存在;请改用Assertions.assertThrows(…)
。-
如果您仍然需要使用
ExpectedException
,请参阅 有限的 JUnit 4 Rule 支持。
-
-
JUnit Jupiter 中的断言和假设接受失败消息作为其最后一个参数,而不是第一个参数。
-
有关详细信息,请参阅 失败消息参数。
-
3.4. 有限的 JUnit 4 Rule 支持
如上所述,JUnit Jupiter 不支持也不会原生支持 JUnit 4 rules。但是,JUnit 团队意识到,许多组织(尤其是大型组织)可能拥有大量使用自定义 rules 的 JUnit 4 代码库。为了服务于这些组织并实现平稳的迁移路径,JUnit 团队已决定在 JUnit Jupiter 中逐字支持精选的 JUnit 4 rules。此支持基于适配器,并且仅限于那些在语义上与 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
启用这种有限形式的 Rule
支持。此注解是一个组合注解,它启用所有 rule 迁移支持扩展:VerifierSupport
、ExternalResourceSupport
和 ExpectedExceptionSupport
。您可以选择使用 @EnableJUnit4MigrationSupport
注解您的测试类,这将注册对 rules 和 JUnit 4 的 @Ignore
注解的迁移支持(请参阅 JUnit 4 @Ignore 支持)。
但是,如果您打算为 JUnit Jupiter 开发新的扩展,请使用 JUnit Jupiter 的新扩展模型,而不是 JUnit 4 的基于 rule 的模型。
3.5. JUnit 4 @Ignore 支持
为了提供从 JUnit 4 到 JUnit Jupiter 的平滑迁移路径,junit-jupiter-migrationsupport
模块为 JUnit 4 的 @Ignore
注解提供了类似于 Jupiter 的 @Disabled
注解的支持。
要将 @Ignore
与基于 JUnit Jupiter 的测试一起使用,请在你的构建中配置对 junit-jupiter-migrationsupport
模块的测试依赖,然后使用 @ExtendWith(IgnoreCondition.class)
或 @EnableJUnit4MigrationSupport
注解你的测试类(它会自动注册 IgnoreCondition
以及 有限的 JUnit 4 Rule 支持)。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.6. 失败消息参数
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)
。这样做的理由是失败消息是可选的,可选参数应在方法签名中的必需参数之后声明。
受此更改影响的方法如下
-
断言 (Assertions)
-
assertTrue
-
assertFalse
-
assertNull
-
assertNotNull
-
assertEquals
-
assertNotEquals
-
assertArrayEquals
-
assertSame
-
assertNotSame
-
assertThrows
-
-
假设 (Assumptions)
-
assumeTrue
-
assumeFalse
-
4. 运行测试
4.1. IDE 支持
4.1.1. IntelliJ IDEA
自 2016.2 版本起,IntelliJ IDEA 支持在 JUnit Platform 上运行测试。有关更多信息,请查阅此 IntelliJ IDEA 资源。但是请注意,建议使用 IDEA 2017.3 或更高版本,因为较新版本的 IDEA 会根据项目中使用的 API 版本自动下载以下 JAR:junit-platform-launcher
、junit-jupiter-engine
和 junit-vintage-engine
。
IDEA 2017.3 之前的 IntelliJ IDEA 版本捆绑了特定版本的 JUnit 5。因此,如果你想使用较新版本的 JUnit Jupiter,则由于版本冲突,在 IDE 中执行测试可能会失败。在这种情况下,请按照以下说明使用比 IntelliJ IDEA 捆绑的版本更新的 JUnit 5 版本。 |
为了使用不同的 JUnit 5 版本(例如,5.12.0),你可能需要将相应版本的 junit-platform-launcher
、junit-jupiter-engine
和 junit-vintage-engine
JAR 包含在类路径中。
testImplementation(platform("org.junit:junit-bom:5.12.0"))
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.12.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
4.1.2. Eclipse
自 Eclipse Oxygen.1a (4.7.1a) 版本起,Eclipse IDE 提供对 JUnit Platform 的支持。
有关在 Eclipse 中使用 JUnit 5 的更多信息,请查阅 Eclipse Project Oxygen.1a (4.7.1a) - 新特性和值得关注 文档的官方 Eclipse 对 JUnit 5 的支持 部分。
4.1.3. NetBeans
自 Apache NetBeans 10.0 版本起,NetBeans 提供对 JUnit Jupiter 和 JUnit Platform 的支持。
有关更多信息,请查阅 Apache NetBeans 10.0 发行说明 的 JUnit 5 部分。
4.1.4. Visual Studio Code
Visual Studio Code 通过 Java Test Runner 扩展(默认安装为 Java Extension Pack 的一部分)支持 JUnit Jupiter 和 JUnit Platform。
有关更多信息,请查阅 Visual Studio Code 中的 Java 文档的 测试 部分。
4.1.5. 其他 IDE
如果你使用的编辑器或 IDE 不是前面部分列出的那些,JUnit 团队提供了两种替代解决方案来帮助你使用 JUnit 5。你可以手动使用 Console Launcher — 例如,从命令行 — 或者如果你的 IDE 内置支持 JUnit 4,则可以使用基于 JUnit 4 的 Runner 执行测试。
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.12.0"))
}
使用 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.12.0") // version can be omitted when using the BOM
}
JUnit Platform 可以运行基于 JUnit 4 的测试,前提是你配置对 JUnit 4 的 testImplementation
依赖,以及对 JUnit Vintage TestEngine
实现的 testRuntimeOnly
依赖,类似于以下内容。
dependencies {
testImplementation("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.12.0") // version can be omitted when using the BOM
}
配置日志记录(可选)
JUnit 在 java.util.logging
包(又名 JUL)中使用 Java Logging API 来发出警告和调试信息。有关配置选项,请参阅 LogManager
的官方文档。
或者,可以将日志消息重定向到其他日志框架,例如 Log4j 或 Logback。要使用提供 LogManager
自定义实现的日志框架,请将 java.util.logging.manager
系统属性设置为要使用的 LogManager
实现的完全限定类名。以下示例演示了如何配置 Log4j 2.x(有关详细信息,请参阅 Log4j JDK Logging Adapter)。
test {
systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
// Avoid overhead (see https://logging.apache.ac.cn/log4j/2.x/manual/jmx.html#enabling-jmx)
systemProperty("log4j2.disableJmx", "true")
}
其他日志框架提供了不同的方法来重定向使用 java.util.logging
记录的消息。例如,对于 Logback,你可以通过向运行时类路径添加额外的依赖项来使用 JUL to SLF4J Bridge。
4.2.2. Maven
Maven Surefire 和 Maven Failsafe 为在 JUnit Platform 上执行测试提供了 原生支持。junit5-jupiter-starter-maven
项目中的 pom.xml
文件演示了如何使用 Maven Surefire 插件,并且可以作为配置 Maven 构建的起点。
使用最新版本的 Maven Surefire/Failsafe 以避免互操作性问题
为了避免互操作性问题,建议使用最新版本的 Maven Surefire/Failsafe(3.0.0 或更高版本),因为它会自动对齐使用的 JUnit Platform Launcher 的版本与在测试运行时类路径上找到的 JUnit Platform 版本。 如果你使用的是低于 3.0.0-M4 的版本,你可以通过在 Maven 构建中添加对匹配版本的 JUnit Platform Launcher 的测试依赖来解决缺少对齐的问题,如下所示。
|
对齐依赖版本
除非你正在使用 Spring Boot,它定义了自己的依赖管理方式,否则建议使用 JUnit Platform BOM 来对齐所有 JUnit 5 组件的版本。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.12.0</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.12.0</version> <!-- can be omitted when using the BOM -->
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
Maven Surefire 和 Maven Failsafe 可以与 Jupiter 测试一起运行基于 JUnit 4 的测试,前提是你配置对 JUnit 4 的 test
作用域的依赖,以及对 JUnit Vintage TestEngine
实现的依赖,类似于以下内容。
<!-- ... -->
<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.12.0</version> <!-- can be omitted when using the BOM -->
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.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.5.2</version>
<configuration>
<excludes>
<exclude/>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
有关详细信息,请参阅 Maven Surefire 的 测试的包含和排除 文档。
按标签过滤
-
要包含标签或标签表达式,请使用
groups
。 -
要排除标签或标签表达式,请使用
excludedGroups
。
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.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.5.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 中 fork 测试。
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.12.0'
使用 Maven,你可以通过在 pom.xml
文件中包含以下内容来覆盖 JUnit Jupiter 版本。
<properties>
<junit-jupiter.version>5.12.0</junit-jupiter.version>
</properties>
4.3. Console Launcher
ConsoleLauncher
是一个命令行 Java 应用程序,可让你从控制台启动 JUnit Platform。例如,它可以用于运行 JUnit Vintage 和 JUnit Jupiter 测试,并将测试执行结果打印到控制台。
包含所有依赖项内容的 Fat JAR (junit-platform-console-standalone-1.12.0.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.12.0
-
org.junit.jupiter:junit-jupiter-engine:5.12.0
-
org.junit.jupiter:junit-jupiter-params:5.12.0
-
org.junit.platform:junit-platform-commons:1.12.0
-
org.junit.platform:junit-platform-console:1.12.0
-
org.junit.platform:junit-platform-engine:1.12.0
-
org.junit.platform:junit-platform-launcher:1.12.0
-
org.junit.platform:junit-platform-reporting:1.12.0
-
org.junit.platform:junit-platform-suite-api:1.12.0
-
org.junit.platform:junit-platform-suite-commons:1.12.0
-
org.junit.platform:junit-platform-suite-engine:1.12.0
-
org.junit.vintage:junit-vintage-engine:5.12.0
-
org.opentest4j:opentest4j:1.3.0
由于 此外,你不太可能需要在项目的 Maven POM 或 Gradle 构建脚本中包含对 如果你需要在构建脚本中声明对 |
你可以 运行 独立的 ConsoleLauncher
,如下所示。
$ java -jar junit-platform-console-standalone-1.12.0.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. -h, --help Display help information. --version Display version information. --disable-ansi-colors Disable ANSI colors in output (not supported by all terminals). 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.cn/junit5/docs/1.12.0/user-guide/
发现测试
Usage: junit discover [OPTIONS] Discover tests [@<filename>...] One or more argument files containing options. --disable-ansi-colors Disable ANSI colors in output (not supported by all terminals). --disable-banner Disable print out of the welcome message. -h, --help Display help information. --version Display version 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. The line and column numbers can be provided as URI query parameters (e.g. foo.txt? line=12&column=34). 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=PREFIX:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]... Select iterations for test discovery via a prefixed identifier and a list of indexes or index ranges (e.g. method:com.acme. Foo#m()[1..2] selects the first and second iteration of the m() method in the com.acme.Foo class). This option can be repeated. --uid, --select-unique-id=UNIQUE-ID... Select a unique id for test discovery. This option can be repeated. --select=PREFIX:VALUE... Select via a prefixed identifier (e.g. method:com.acme.Foo#m selects the m() method in the com.acme.Foo class). This option can be repeated. For more information on selectors including syntax examples, see https://junit.cn/junit5/docs/current/user-guide/#running-tests-discovery-selectors 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. --include-methodname=PATTERN Provide a regular expression to include only methods whose fully qualified names without parameters match. When this option is repeated, all patterns will be combined using OR semantics. --exclude-methodname=PATTERN Provide a regular expression to exclude those methods whose fully qualified names without parameters match. When this option is repeated, all patterns will be combined using OR semantics. -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-resource=PATH Set configuration parameters for test discovery and execution via a classpath resource. 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.cn/junit5/docs/1.12.0/user-guide/
执行测试
Usage: junit execute [OPTIONS] Execute tests [@<filename>...] One or more argument files containing options. --disable-ansi-colors Disable ANSI colors in output (not supported by all terminals). --disable-banner Disable print out of the welcome message. -h, --help Display help information. --version Display version 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. The line and column numbers can be provided as URI query parameters (e.g. foo.txt? line=12&column=34). 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=PREFIX:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]... Select iterations for test discovery via a prefixed identifier and a list of indexes or index ranges (e.g. method:com.acme. Foo#m()[1..2] selects the first and second iteration of the m() method in the com.acme.Foo class). This option can be repeated. --uid, --select-unique-id=UNIQUE-ID... Select a unique id for test discovery. This option can be repeated. --select=PREFIX:VALUE... Select via a prefixed identifier (e.g. method:com.acme.Foo#m selects the m() method in the com.acme.Foo class). This option can be repeated. For more information on selectors including syntax examples, see https://junit.cn/junit5/docs/current/user-guide/#running-tests-discovery-selectors 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. --include-methodname=PATTERN Provide a regular expression to include only methods whose fully qualified names without parameters match. When this option is repeated, all patterns will be combined using OR semantics. --exclude-methodname=PATTERN Provide a regular expression to exclude those methods whose fully qualified names without parameters match. When this option is repeated, all patterns will be combined using OR semantics. -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-resource=PATH Set configuration parameters for test discovery and execution via a classpath resource. 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.cn/junit5/docs/1.12.0/user-guide/
列出测试引擎
Usage: junit engines [OPTIONS] List available test engines [@<filename>...] One or more argument files containing options. --disable-ansi-colors Disable ANSI colors in output (not supported by all terminals). --disable-banner Disable print out of the welcome message. -h, --help Display help information. --version Display version information. For more information, please refer to the JUnit User Guide at https://junit.cn/junit5/docs/1.12.0/user-guide/
4.3.2. 参数文件 (@-files)
在某些平台上,当创建包含大量选项或长参数的命令行时,你可能会遇到系统对命令行长度的限制。
自 1.3 版本起,ConsoleLauncher
支持参数文件,也称为 @-files。参数文件是本身包含要传递给命令的参数的文件。当底层 picocli 命令行解析器遇到以字符 @
开头的参数时,它会将该文件的内容扩展到参数列表中。
文件中的参数可以用空格或换行符分隔。如果参数包含嵌入的空格,则应将整个参数用双引号或单引号括起来 — 例如,"-f=My Files/Stuff.java"
。
如果参数文件不存在或无法读取,则该参数将被按字面意思处理,并且不会被删除。这很可能会导致“unmatched argument”错误消息。你可以通过将 picocli.trace
系统属性设置为 DEBUG
来排查此类错误。
可以在命令行上指定多个 @-files。指定的路径可以是相对于当前目录的路径,也可以是绝对路径。
你可以通过使用额外的 @
符号转义来传递带有初始 @
字符的真实参数。例如,@@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 runner 已被弃用
近年来,所有主流构建工具和 IDE 都提供了内置支持,可以直接在 JUnit Platform 上运行测试。 此外,由 因此, 如果你正在使用 |
JUnitPlatform
runner 是一个基于 JUnit 4 的 Runner
,它使你能够在 JUnit 4 环境中运行任何编程模型在 JUnit Platform 上受支持的测试 — 例如,JUnit Jupiter 测试类。
使用 @RunWith(JUnitPlatform.class)
注解类允许它与支持 JUnit 4 但尚不支持 JUnit Platform 的 IDE 和构建系统一起运行。
由于 JUnit Platform 具有 JUnit 4 没有的功能,因此 runner 只能支持 JUnit Platform 功能的子集,尤其是在报告方面(请参阅 显示名称与技术名称)。 |
4.4.1. 设置
你需要在类路径上具有以下组件及其依赖项。有关组 ID、组件 ID 和版本的详细信息,请参阅 依赖元数据。
4.4.2. 显示名称与技术名称
要为通过 @RunWith(JUnitPlatform.class)
运行的类定义自定义显示名称,请使用 @SuiteDisplayName
注解该类并提供自定义值。
默认情况下,显示名称将用于测试组件;但是,当使用 JUnitPlatform
runner 通过构建工具(例如 Gradle 或 Maven)执行测试时,生成的测试报告通常需要包含测试组件的技术名称 — 例如,完全限定类名 — 而不是较短的显示名称,例如测试类的简单名称或包含特殊字符的自定义显示名称。要为报告目的启用技术名称,请在 @RunWith(JUnitPlatform.class)
旁边声明 @UseTechnicalNames
注解。
请注意,@UseTechnicalNames
的存在会覆盖通过 @SuiteDisplayName
配置的任何自定义显示名称。
4.4.3. 单个测试类
使用 JUnitPlatform
runner 的一种方法是直接使用 @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 Platform 提供了一组丰富的发现选择器,可用于指定应发现或执行哪些测试。
发现选择器可以使用 DiscoverySelectors
类中的工厂方法以编程方式创建,在使用 JUnit Platform Suite Engine 时通过注解声明性地指定,通过 Console Launcher 的选项指定,或通过其标识符以字符串形式泛型地指定。
开箱即用地提供了以下发现选择器
Java 类型 | API | 注解 | 控制台启动器 | 标识符 |
---|---|---|---|---|
|
|
|||
— |
|
|
||
|
|
|||
|
|
|||
|
|
|||
|
|
|
||
|
|
|||
|
|
|||
|
|
|
||
|
|
|
||
|
|
|||
|
|
|
||
|
|
4.6. 配置参数
除了指示平台要包含哪些测试类和测试引擎、要扫描哪些包等之外,有时还需要提供特定于特定测试引擎、监听器或已注册扩展的其他自定义配置参数。例如,JUnit Jupiter TestEngine
支持以下用例的配置参数。
配置参数是基于文本的键值对,可以通过以下机制之一提供给在 JUnit Platform 上运行的测试引擎。
-
LauncherDiscoveryRequestBuilder
中的configurationParameter()
和configurationParameters()
方法,用于构建提供给 Launcher API 的请求。
当通过 JUnit Platform 提供的工具之一运行测试时,你可以按如下方式指定配置参数-
Console Launcher:使用
--config
命令行选项。 -
Gradle:使用
systemProperty
或systemProperties
DSL。 -
Maven Surefire provider:使用
configurationParameters
属性。
-
-
LauncherDiscoveryRequestBuilder
中的configurationParametersResources()
方法。
当通过 Console Launcher 运行测试时,你可以使用--config-resource
命令行选项指定自定义配置文件。 -
JVM 系统属性。
-
JUnit Platform 默认配置文件:类路径根目录中名为
junit-platform.properties
的文件,该文件遵循 JavaProperties
文件的语法规则。
配置参数按照上面定义的精确顺序查找。因此,直接提供给 Launcher 的配置参数优先于通过自定义配置文件、系统属性和默认配置文件提供的配置参数。同样,通过系统属性提供的配置参数优先于通过默认配置文件提供的配置参数。 |
4.6.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.7. 标签
标签是 JUnit Platform 的一个概念,用于标记和过滤测试。向容器和测试添加标签的编程模型由测试框架定义。例如,在基于 JUnit Jupiter 的测试中,应该使用 @Tag
注解(参见 标签和过滤)。对于基于 JUnit 4 的测试,Vintage 引擎将 @Category
注解映射到标签(参见 Categories Support)。其他测试框架可能会定义他们自己的注解或其他方式供用户指定标签。
4.7.1. 标签的语法规则
无论标签是如何指定的,JUnit Platform 都会强制执行以下规则
-
标签不能为空(
null
)或空白。 -
修剪后的标签不得包含空格。
-
修剪后的标签不得包含 ISO 控制字符。
-
修剪后的标签不得包含以下任何保留字符。
-
,
: 逗号 -
(
: 左括号 -
)
: 右括号 -
&
: 与号 -
|
: 竖线 -
!
: 感叹号
-
在上述上下文中,“修剪后”表示已删除前导和尾随空格字符。 |
4.7.2. 标签表达式
标签表达式是布尔表达式,运算符包括 !
、&
和 |
。此外,可以使用 (
和 )
来调整运算符优先级。
支持两个特殊表达式,any()
和 none()
,它们分别选择所有带有任何标签的测试和所有不带任何标签的测试。这些特殊表达式可以像普通标签一样与其他表达式组合使用。
运算符 | 含义 | 结合性 |
---|---|---|
|
非 |
右 |
|
与 |
左 |
|
或 |
左 |
如果您在多个维度上标记测试,标签表达式可以帮助您选择要执行的测试。当按测试类型(例如,微服务、集成、端到端)和功能(例如,产品、目录、发货)进行标记时,以下标签表达式可能会很有用。
标签表达式 | 选择 |
---|---|
|
所有针对 product(产品) 的测试 |
|
所有针对 catalog(目录) 的测试加上所有针对 shipping(发货) 的测试 |
|
所有针对 catalog(目录) 和 shipping(发货) 之间交集的测试 |
|
所有针对 product(产品) 的测试,但不包括 端到端 测试 |
|
所有针对 product(产品) 或 shipping(发货) 的 微服务 或 集成 测试 |
4.8. 捕获标准输出/错误
自 1.3 版本起,JUnit Platform 提供了选择性启用功能,用于捕获打印到 System.out
和 System.err
的输出。要启用它,请将 junit.platform.output.capture.stdout
和/或 junit.platform.output.capture.stderr
配置参数设置为 true
。此外,您可以使用 junit.platform.output.capture.maxBuffer
配置每个执行的测试或容器要使用的最大缓冲字节数。
如果启用,JUnit Platform 将捕获相应的输出,并立即在报告测试或容器完成之前,使用 stdout
或 stderr
键将其作为报告条目发布到所有已注册的 TestExecutionListener
实例。
请注意,捕获的输出将仅包含用于执行容器或测试的线程发出的输出。其他线程的任何输出都将被省略,因为特别是在并行执行测试时,将无法将其归因于特定的测试或容器。
4.9. 使用监听器和拦截器
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
可以通过 Console Launcher 使用或手动注册,以生成与基于 JUnit 4 的测试报告的事实标准兼容的 XML 报告。OpenTestReportGeneratingListener
以 Open Test Reporting 指定的基于事件的格式生成 XML 报告。它已自动注册,可以通过 配置参数 启用和配置。有关详细信息,请参阅 JUnit Platform 报告。
- Flight Recorder 支持
-
FlightRecordingExecutionListener
和FlightRecordingDiscoveryListener
在测试发现和执行期间生成 Java Flight Recorder 事件。 LoggingListener
-
TestExecutionListener
用于通过BiConsumer
记录所有事件的信息性消息,该BiConsumer
消费Throwable
和Supplier<String>
。 SummaryGeneratingListener
-
TestExecutionListener
生成测试执行的摘要,可以通过PrintWriter
打印。 UniqueIdTrackingListener
-
TestExecutionListener
跟踪在TestPlan
执行期间跳过或执行的所有测试的唯一 ID,并在TestPlan
执行完成后生成包含唯一 ID 的文件。
4.9.1. Flight Recorder 支持
自 1.7 版本起,JUnit Platform 提供了选择性启用功能,用于生成 Flight Recorder 事件。JEP 328 将 Java Flight Recorder (JFR) 描述为
Flight Recorder 记录源自应用程序、JVM 和 OS 的事件。事件存储在单个文件中,该文件可以附加到错误报告并由支持工程师检查,从而可以对问题发生前一段时间内的问题进行事后分析。 |
为了记录在运行测试时生成的 Flight Recorder 事件,您需要
-
确保您使用的是 Java 8 Update 262 或更高版本,或者 Java 11 或更高版本。
-
在测试运行时,在类路径或模块路径上提供
org.junit.platform.jfr
模块 (junit-platform-jfr-1.12.0.jar
)。 -
在启动测试运行时启动 Flight Recorder 记录。Flight Recorder 可以通过 java 命令行选项启动
-XX:StartFlightRecording:filename=...
请查阅您的构建工具手册以获取相应的命令。
要分析记录的事件,请使用最新 JDK 附带的 jfr 命令行工具,或使用 JDK Mission Control 打开记录文件。
Flight Recorder 支持目前是一项实验性功能。我们邀请您试用并向 JUnit 团队提供反馈,以便他们可以改进并最终推广此功能。 |
4.10. 堆栈跟踪剪枝
自 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
: 支持静态字段注入 -
TestInstancePostProcessor
: 支持非静态字段注入 -
ParameterResolver
: 支持构造函数和方法注入
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.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.junit.platform.commons.support.ModifierSupport;
class RandomNumberExtension
implements BeforeAllCallback, TestInstancePostProcessor, 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 postProcessTestInstance(Object testInstance, ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
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 字段不能为空(在评估时),但可以是 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 {
@JvmField
@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
在方法级别注册的扩展之前注册。
在以下示例中,测试类中的 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.2.4. 扩展继承
注册的扩展在测试类层次结构中以自顶向下的语义继承。同样,在类级别注册的扩展在方法级别继承。这适用于所有扩展,无论它们是如何注册的(声明式或编程式)。
这意味着通过超类上的 @ExtendWith
声明式注册的扩展将在通过子类上的 @ExtendWith
声明式注册的扩展之前注册。
同样,通过超类中字段上的 @RegisterExtension
或 @ExtendWith
编程式注册的扩展将在通过子类中字段上的 @RegisterExtension
或 @ExtendWith
编程式注册的扩展之前注册,除非使用 @Order
来更改该行为(有关详细信息,请参阅 扩展注册顺序)。
对于给定的扩展上下文及其父上下文,特定的扩展实现只能注册一次。因此,任何注册重复扩展实现的尝试都将被忽略。 |
5.3. 条件测试执行
ExecutionCondition
定义了用于编程式条件测试执行的 Extension API。
对于每个容器(例如,测试类),都会评估 ExecutionCondition
,以确定是否应根据提供的 ExtensionContext
执行其包含的所有测试。同样,对于每个测试,都会评估 ExecutionCondition
,以确定是否应根据提供的 ExtensionContext
执行给定的测试方法。
当注册了多个 ExecutionCondition
扩展时,只要其中一个条件返回禁用,容器或测试就会被禁用。因此,不能保证会评估某个条件,因为另一个扩展可能已经导致容器或测试被禁用。换句话说,评估的工作方式类似于短路布尔 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
定义了 Extension 的 API,这些 Extension 希望在构造测试实例之前(通过构造函数调用或通过 TestInstanceFactory
)被调用。
此扩展提供对 TestInstancePreDestroyCallback
的对称调用,并且与其他扩展结合使用以准备构造函数参数或跟踪测试实例及其生命周期非常有用。
访问测试作用域的 ExtensionContext
您可以覆盖 |
5.5. 测试实例工厂
TestInstanceFactory
定义了 Extension 的 API,这些 Extension 希望创建测试类实例。
常见的用例包括从依赖注入框架获取测试实例或调用静态工厂方法来创建测试类实例。
如果未注册 TestInstanceFactory
,则框架将调用测试类的唯一构造函数来实例化它,并可能通过注册的 ParameterResolver
扩展来解析构造函数参数。
实现 TestInstanceFactory
的扩展可以在测试接口、顶级测试类或 @Nested
测试类上注册。
为任何单个类注册多个实现 |
访问测试作用域的 ExtensionContext
您可以覆盖 |
5.6. 测试实例后处理
TestInstancePostProcessor
定义了 Extension 的 API,这些 Extension 希望后处理测试实例。
常见的用例包括将依赖项注入到测试实例中,调用测试实例上的自定义初始化方法等。
有关具体示例,请查阅 MockitoExtension
和 SpringExtension
的源代码。
访问测试作用域的 ExtensionContext
您可以覆盖 |
5.7. 测试实例预销毁回调
TestInstancePreDestroyCallback
定义了 Extension 的 API,这些 Extension 希望在测试实例已在测试中使用之后并在销毁之前对其进行处理。
常见的用例包括清理已注入到测试实例中的依赖项,调用测试实例上的自定义反初始化方法等。
5.8. 参数解析
ParameterResolver
定义了用于在运行时动态解析参数的 Extension API。
如果测试类构造函数、测试方法或生命周期方法(请参阅 定义)声明了一个参数,则该参数必须在运行时由 ParameterResolver
解析。ParameterResolver
可以是内置的(请参阅 TestInfoParameterResolver
)或 由用户注册。一般来说,参数可以通过名称、类型、注解或它们的任意组合来解析。
如果您希望实现一个自定义 ParameterResolver
,该 ParameterResolver
仅基于参数类型解析参数,您可能会发现扩展 TypeBasedParameterResolver
很方便,它可以用作此类用例的通用适配器。
例如,要查看具体的示例,请查阅 CustomTypeParameterResolver
、CustomAnnotationParameterResolver
和 MapOfListsTypeBasedParameterResolver
的源代码。
由于 JDK 9 之前版本中的 因此,提供给
|
访问测试作用域的 ExtensionContext
您可以重写 |
从扩展调用的方法进行参数解析
其他扩展也可以利用已注册的 |
5.8.1. 参数冲突
如果为某个测试注册了多个支持相同类型的 ParameterResolver
实现,则会抛出 ParameterResolutionException
,并显示一条消息,指示已发现竞争的解析器。请参见以下示例
public class ParameterResolverConflictDemo {
@Test
@ExtendWith({ FirstIntegerResolver.class, SecondIntegerResolver.class })
void testInt(int i) {
// Test will not run due to ParameterResolutionException
assertEquals(1, i);
}
static class FirstIntegerResolver 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 1;
}
}
static class SecondIntegerResolver 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;
}
}
}
如果冲突的 ParameterResolver
实现应用于不同的测试方法,如下例所示,则不会发生冲突。
public class ParameterResolverNoConflictDemo {
@Test
@ExtendWith(FirstIntegerResolver.class)
void firstResolution(int i) {
assertEquals(1, i);
}
@Test
@ExtendWith(SecondIntegerResolver.class)
void secondResolution(int i) {
assertEquals(2, i);
}
static class FirstIntegerResolver 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 1;
}
}
static class SecondIntegerResolver 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;
}
}
}
如果需要将冲突的 ParameterResolver
实现应用于相同的测试方法,您可以实现自定义类型或自定义注解,如 CustomTypeParameterResolver
和 CustomAnnotationParameterResolver
分别所示。
public class ParameterResolverCustomTypeDemo {
@Test
@ExtendWith({ FirstIntegerResolver.class, SecondIntegerResolver.class })
void testInt(Integer i, WrappedInteger wrappedInteger) {
assertEquals(1, i);
assertEquals(2, wrappedInteger.value);
}
static class FirstIntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(Integer.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 1;
}
}
static class SecondIntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(WrappedInteger.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return new WrappedInteger(2);
}
}
static class WrappedInteger {
private final int value;
public WrappedInteger(int value) {
this.value = value;
}
}
}
自定义注解使重复类型与其对应类型区分开来
public class ParameterResolverCustomAnnotationDemo {
@Test
void testInt(@FirstInteger Integer first, @SecondInteger Integer second) {
assertEquals(1, first);
assertEquals(2, second);
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(FirstInteger.Extension.class)
public @interface FirstInteger {
class Extension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(Integer.class)
&& !parameterContext.isAnnotated(SecondInteger.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 1;
}
}
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SecondInteger.Extension.class)
public @interface SecondInteger {
class Extension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.isAnnotated(SecondInteger.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return 2;
}
}
}
}
JUnit 包含一些内置的参数解析器,如果解析器尝试声明其支持的类型,可能会导致冲突。例如,TestInfo
提供有关测试的元数据。有关详细信息,请参阅构造函数和方法的依赖注入。诸如 Spring 之类的第三方框架也可能定义参数解析器。应用本节中的一种技术来解决任何冲突。
参数化测试是另一个潜在的冲突来源。确保使用 @ParameterizedTest
注解的测试没有同时使用 @Test
注解,并参阅使用参数以获取更多详细信息。
5.9. 测试结果处理
TestWatcher
定义了扩展的 API,这些扩展希望处理测试方法执行的结果。具体来说,对于以下事件,将使用上下文信息调用 TestWatcher
。
-
testDisabled
:在禁用的测试方法被跳过后调用 -
testSuccessful
:在测试方法成功完成后调用 -
testAborted
:在测试方法被中止后调用 -
testFailed
:在测试方法失败后调用
与定义中提出的“测试方法”的定义相反,在此上下文中,测试方法指的是任何 @Test 方法或 @TestTemplate 方法(例如,@RepeatedTest 或 @ParameterizedTest )。 |
实现此接口的扩展可以在类级别、实例级别或方法级别注册。在类级别注册时,对于任何包含的测试方法(包括 @Nested
类中的方法),都会调用 TestWatcher
。在方法级别注册时,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,这些扩展希望添加在测试方法执行之前和之后立即执行的行为。因此,这些回调非常适合计时、跟踪和类似用例。如果您需要实现围绕 @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 为希望通过 TestExecutionExceptionHandler
处理 @Test
方法期间抛出的异常的 Extensions
,以及为那些通过 LifecycleMethodExecutionExceptionHandler
处理测试生命周期方法(@BeforeAll
、@BeforeEach
、@AfterEach
和 @AfterAll
)期间抛出的异常的 Extensions
提供了 API。
以下示例展示了一个扩展,该扩展将吞噬所有 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 引擎,就好像从未抛出异常一样。处理程序也可以选择重新抛出异常或抛出不同的异常,可能会包装原始异常。
希望处理 @BeforeAll
或 @AfterAll
期间抛出的异常的 LifecycleMethodExecutionExceptionHandler
的实现需要在类级别注册,而 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. 预中断回调
PreInterruptCallback
定义了 Extensions
的 API,这些扩展希望在调用 Thread.interrupt()
之前对超时做出反应。
有关更多信息,请参阅调试超时。
5.13. 拦截调用
InvocationInterceptor
定义了 Extensions
的 API,这些扩展希望拦截对测试代码的调用。
以下示例展示了一个扩展,该扩展在 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;
}
}
}
访问测试作用域的 ExtensionContext
您可以重写 |
5.14. 为测试模板提供调用上下文
只有在至少注册了一个 TestTemplateInvocationContextProvider
时,才能执行 @TestTemplate
方法。每个此类提供程序都负责提供 TestTemplateInvocationContext
实例的 Stream
。每个上下文都可以指定自定义显示名称和额外的扩展列表,这些扩展将仅用于下一次 @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.15. 在扩展中保持状态
通常,扩展只实例化一次。因此,问题变得相关:如何保持从一个扩展调用到下一个调用的状态?ExtensionContext
API 正是为此目的提供了一个 Store
。扩展可以将值放入存储中以供以后检索。请参阅 TimingExtension
,以获取将 Store
与方法级别范围一起使用的示例。重要的是要记住,在测试执行期间存储在 ExtensionContext
中的值在周围的 ExtensionContext
中将不可用。由于 ExtensionContexts
可能是嵌套的,因此内部上下文的范围也可能受到限制。有关通过 Store
存储和检索值的可用方法的详细信息,请查阅相应的 Javadoc。
ExtensionContext.Store.CloseableResource CloseableResource 实例)都会通过调用其 close() 方法(以添加的相反顺序)收到通知。 |
下面显示了 CloseableResource
的示例实现,使用 HttpServer
资源。
CloseableResource
的 HttpServer
资源class HttpServerResource implements CloseableResource {
private final HttpServer httpServer;
HttpServerResource(int port) throws IOException {
InetAddress loopbackAddress = InetAddress.getLoopbackAddress();
this.httpServer = HttpServer.create(new InetSocketAddress(loopbackAddress, port), 0);
}
HttpServer getHttpServer() {
return httpServer;
}
void start() {
// Example handler
httpServer.createContext("/example", exchange -> {
String body = "This is a test";
exchange.sendResponseHeaders(200, body.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(body.getBytes(UTF_8));
}
});
httpServer.setExecutor(null);
httpServer.start();
}
@Override
public void close() {
httpServer.stop(0);
}
}
然后,可以将此资源存储在所需的 ExtensionContext
中。如果需要,可以将其存储在类或方法级别,但这可能会为此类资源增加不必要的开销。对于此示例,明智的做法可能是将其存储在根级别并延迟实例化,以确保每个测试运行仅创建一次,并在不同的测试类和方法之间重用。
Store.getOrComputeIfAbsent
在根上下文中延迟存储public class HttpServerExtension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return HttpServer.class.equals(parameterContext.getParameter().getType());
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
ExtensionContext rootContext = extensionContext.getRoot();
ExtensionContext.Store store = rootContext.getStore(Namespace.GLOBAL);
String key = HttpServerResource.class.getName();
HttpServerResource resource = store.getOrComputeIfAbsent(key, __ -> {
try {
HttpServerResource serverResource = new HttpServerResource(0);
serverResource.start();
return serverResource;
}
catch (IOException e) {
throw new UncheckedIOException("Failed to create HttpServerResource", e);
}
}, HttpServerResource.class);
return resource.getHttpServer();
}
}
HttpServerExtension
的测试用例@ExtendWith(HttpServerExtension.class)
public class HttpServerDemo {
@Test
void httpCall(HttpServer server) throws Exception {
String hostName = server.getAddress().getHostName();
int port = server.getAddress().getPort();
String rawUrl = String.format("http://%s:%d/example", hostName, port);
URL requestUrl = URI.create(rawUrl).toURL();
String responseBody = sendRequest(requestUrl);
assertEquals("This is a test", responseBody);
}
private static String sendRequest(URL url) throws IOException {
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
int contentLength = connection.getContentLength();
try (InputStream response = url.openStream()) {
byte[] content = new byte[contentLength];
assertEquals(contentLength, response.read(content));
return new String(content, UTF_8);
}
}
}
5.16. 扩展中支持的实用程序
junit-platform-commons
工件提供了用于处理注解、类、反射、类路径扫描和转换任务的维护实用程序。这些实用程序可以在 org.junit.platform.commons.support
及其子包中找到。建议 TestEngine
和 Extension
作者使用这些受支持的实用程序,以便与 JUnit Platform 和 JUnit Jupiter 的行为保持一致。
5.16.1. 注解支持
AnnotationSupport
提供了静态实用程序方法,这些方法对注解元素(例如,包、注解、类、接口、构造函数、方法和字段)进行操作。这些方法包括检查元素是否使用特定注解或元注解进行注解、搜索特定注解以及查找类或接口中的注解方法和字段的方法。其中一些方法在已实现的接口和类层次结构中搜索以查找注解。有关更多详细信息,请查阅 AnnotationSupport
的 Javadoc。
isAnnotated() 方法不会查找可重复注解。要检查可重复注解,请使用 findRepeatableAnnotations() 方法之一,并验证返回的列表是否不为空。 |
另请参阅:字段和方法搜索语义 |
5.16.2. 类支持
ClassSupport
提供了用于处理类(即 java.lang.Class
的实例)的静态实用程序方法。有关更多详细信息,请查阅 ClassSupport
的 Javadoc。
5.16.3. 反射支持
ReflectionSupport
提供了静态实用程序方法,这些方法增强了标准的 JDK 反射和类加载机制。这些方法包括扫描类路径以搜索与指定谓词匹配的类、加载和创建类的新实例以及查找和调用方法的方法。其中一些方法遍历类层次结构以查找匹配的方法。有关更多详细信息,请查阅 ReflectionSupport
的 Javadoc。
另请参阅:字段和方法搜索语义 |
5.16.4. 修饰符支持
ModifierSupport
提供了用于处理成员和类修饰符的静态实用程序方法——例如,确定成员是否声明为 public
、private
、abstract
、static
等。有关更多详细信息,请查阅 ModifierSupport
的 Javadoc。
5.16.5. 转换支持
ConversionSupport
(在 org.junit.platform.commons.support.conversion
包中)提供了从字符串转换为原始类型及其相应的包装器类型、来自 java.time
包的日期和时间类型以及一些其他常见的 Java 类型(例如 File
、BigDecimal
、BigInteger
、Currency
、Locale
、URI
、URL
、UUID
等)的支持。有关更多详细信息,请查阅 ConversionSupport
的 Javadoc。
5.16.6. 字段和方法搜索语义
AnnotationSupport
和 ReflectionSupport
中的各种方法使用搜索算法,这些算法遍历类型层次结构以查找匹配的字段和方法——例如,AnnotationSupport.findAnnotatedFields(…)
、ReflectionSupport.findMethods(…)
等。
从 JUnit 5.11(JUnit Platform 1.11)开始,字段和方法搜索算法遵循标准的 Java 语义,关于根据 Java 语言的规则,给定的字段或方法是否可见或被重写。
在 JUnit 5.11 之前,字段和方法搜索算法应用了我们现在称为“旧版语义”的内容。旧版语义认为字段和方法会被超类型(超类或接口)中的字段和方法隐藏、遮蔽或取代,这仅仅基于字段的名称或方法的签名,而忽略了关于可见性的实际 Java 语言语义以及确定一个方法是否覆盖另一个方法的规则。
尽管 JUnit 团队建议使用标准搜索语义,但开发人员可以选择通过 junit.platform.reflection.search.useLegacySemantics
JVM 系统属性恢复为旧版语义。
例如,要为字段和方法启用旧版搜索语义,您可以使用以下系统属性启动 JVM。
-Djunit.platform.reflection.search.useLegacySemantics=true
由于该功能的底层性质,junit.platform.reflection.search.useLegacySemantics 标志只能通过 JVM 系统属性设置。它不能通过配置参数设置。 |
5.17. 用户代码和扩展的相对执行顺序
当执行包含一个或多个测试方法的测试类时,除了用户提供的测试和生命周期方法之外,还会调用许多扩展回调。
另请参阅:测试执行顺序 |
5.17.1. 用户和扩展代码
下图说明了用户提供的代码和扩展代码的相对顺序。用户提供的测试和生命周期方法以橙色显示,扩展实现的回调代码以蓝色显示。灰色框表示单个测试方法的执行,并且对于测试类中的每个测试方法都会重复。

下表进一步解释了用户代码和扩展代码图中的十六个步骤。
步骤 | 接口/注解 | 描述 |
---|---|---|
1 |
接口 |
在执行容器的所有测试之前执行的扩展代码 |
2 |
注解 |
在执行容器的所有测试之前执行的用户代码 |
3 |
接口 |
用于处理从 |
4 |
接口 |
在执行每个测试之前执行的扩展代码 |
5 |
注解 |
在执行每个测试之前执行的用户代码 |
6 |
接口 |
用于处理从 |
7 |
接口 |
在执行测试之前立即执行的扩展代码 |
8 |
注解 |
实际测试方法的用户代码 |
9 |
接口 |
用于处理测试期间抛出的异常的扩展代码 |
10 |
接口 |
在测试执行及其相应的异常处理程序之后立即执行的扩展代码 |
11 |
注解 |
在执行每个测试之后执行的用户代码 |
12 |
接口 |
用于处理从 |
13 |
接口 |
在执行每个测试之后执行的扩展代码 |
14 |
注解 |
在执行容器的所有测试之后执行的用户代码 |
15 |
接口 |
用于处理从 |
16 |
接口 |
在执行容器的所有测试之后执行的扩展代码 |
在最简单的情况下,只会执行实际的测试方法(步骤 8);所有其他步骤都是可选的,具体取决于用户代码或扩展是否支持相应的生命周期回调。有关各种生命周期回调的更多详细信息,请查阅每个注解和扩展的相应 Javadoc。
通过实现InvocationInterceptor
,还可以拦截上表中用户代码方法的所有调用。
5.17.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
方法从超类继承,只要它们没有被覆盖。此外,超类中的@BeforeAll
方法将在子类中的@BeforeAll
方法之前执行。-
类似地,在接口中声明的
@BeforeAll
方法会被继承,只要它们没有被覆盖,并且来自接口的@BeforeAll
方法将在实现该接口的类中的@BeforeAll
方法之前执行。
-
-
@AfterAll
方法从超类继承,只要它们没有被覆盖。此外,超类中的@AfterAll
方法将在子类中的@AfterAll
方法之后执行。-
类似地,在接口中声明的
@AfterAll
方法会被继承,只要它们没有被覆盖,并且来自接口的@AfterAll
方法将在实现该接口的类中的@AfterAll
方法之后执行。
-
-
@BeforeEach
方法从超类继承,只要它们没有被覆盖。此外,超类中的@BeforeEach
方法将在子类中的@BeforeEach
方法之前执行。-
类似地,声明为接口默认方法的
@BeforeEach
方法会被继承,只要它们没有被覆盖,并且来自接口的@BeforeEach
默认方法将在实现该接口的类中的@BeforeEach
方法之前执行。
-
-
@AfterEach
方法从超类继承,只要它们没有被覆盖。此外,超类中的@AfterEach
方法将在子类中的@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 Platform 报告
junit-platform-reporting
工件包含 TestExecutionListener
实现,这些实现以两种风格生成 XML 测试报告:Open Test Reporting 和 legacy。
该模块还包含其他 TestExecutionListener 实现,可用于构建自定义报告。有关详细信息,请参阅使用监听器和拦截器。 |
6.1.1. 输出目录
JUnit Platform 通过 EngineDiscoveryRequest
和 TestPlan
为注册的 测试引擎 和 监听器 提供 OutputDirectoryProvider
。其根目录可以通过以下配置参数进行配置
junit.platform.reporting.output.dir=<路径>
-
配置报告的输出目录。默认情况下,如果找到 Gradle 构建脚本,则使用
build
,如果找到 Maven POM,则使用target
;否则,使用当前工作目录。
要为每次测试运行创建一个唯一的输出目录,您可以在路径中使用 {uniqueNumber}
占位符。例如,reports/junit-{uniqueNumber}
将创建类似 reports/junit-8803697269315188212
的目录。当使用 Gradle 或 Maven 的并行执行功能(创建多个并发运行的 JVM 分叉)时,这可能很有用。
6.1.2. Open Test Reporting
OpenTestReportGeneratingListener
以 Open Test Reporting 指定的基于事件的格式为整个执行过程编写 XML 报告,该格式支持 JUnit Platform 的所有功能,例如分层测试结构、显示名称、标签等。
该监听器是自动注册的,可以通过以下配置参数进行配置
junit.platform.reporting.open.xml.enabled=true|false
-
启用/禁用写入报告。
如果启用,监听器将在配置的输出目录中创建一个名为 open-test-report.xml
的 XML 报告文件。
如果启用了输出捕获,则写入 System.out
和 System.err
的捕获输出也将包含在报告中。
Open Test Reporting CLI 工具可用于将基于事件的格式转换为更易于人阅读的分层格式。 |
Gradle
对于 Gradle,可以通过系统属性启用和配置写入 Open Test Reporting 兼容的 XML 报告。以下示例将其输出目录配置为与 Gradle 用于其自身 XML 报告的目录相同。使用 CommandLineArgumentProvider
以保持任务在不同机器之间可重定位,这在使用 Gradle 的构建缓存时非常重要。
dependencies {
testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.12.0")
}
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.12.0")
}
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.12.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.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>
Console Launcher
当使用 Console Launcher 时,您可以通过 --config
设置配置参数来启用 Open Test Reporting 输出
$ java -jar junit-platform-console-standalone-1.12.0.jar <OPTIONS> \
--config=junit.platform.reporting.open.xml.enabled=true \
--config=junit.platform.reporting.output.dir=reports
配置参数也可以在自定义属性文件中设置,该文件作为类路径资源通过 --config-resource
选项提供
$ java -jar junit-platform-console-standalone-1.12.0.jar <OPTIONS> \
--config-resource=configuration.properties
6.1.3. Legacy XML 格式
LegacyXmlReportGeneratingListener
为 TestPlan
中的每个根生成单独的 XML 报告。请注意,生成的 XML 格式与 Ant 构建系统普及的基于 JUnit 4 的测试报告的事实标准兼容。
LegacyXmlReportGeneratingListener
也被 Console Launcher 使用。
6.2. JUnit Platform Suite Engine
JUnit Platform 支持使用 JUnit Platform 从任何测试引擎声明式地定义和执行测试套件。
6.2.1. 设置
除了 junit-platform-suite-api
和 junit-platform-suite-engine
工件之外,您还需要类路径上至少一个其他测试引擎及其依赖项。有关组 ID、工件 ID 和版本的详细信息,请参阅依赖项元数据。
6.2.2. @Suite 示例
通过使用 @Suite
注解类,它在 JUnit Platform 上被标记为测试套件。如以下示例所示,然后可以使用选择器和过滤器注解来控制套件的内容。
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.2.3. @BeforeSuite 和 @AfterSuite
@BeforeSuite
和 @AfterSuite
注解可以在 @Suite
注解的类中的方法上使用。它们将在测试套件的所有测试之前和之后分别执行。
@Suite
@SelectPackages("example")
class BeforeAndAfterSuiteDemo {
@BeforeSuite
static void beforeSuite() {
// executes before the test suite
}
@AfterSuite
static void afterSuite() {
// executes after the test suite
}
}
6.3. JUnit Platform Test Kit
junit-platform-testkit
工件提供对在 JUnit Platform 上执行测试计划然后验证预期结果的支持。从 JUnit Platform 1.12.0 开始,此支持仅限于单个 TestEngine
的执行(请参阅Engine Test Kit)。
6.3.1. Engine Test Kit
org.junit.platform.testkit.engine
包提供对在 JUnit Platform 上运行的给定 TestEngine
执行 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. 断言统计信息
Test Kit 最常见的功能之一是能够针对 TestPlan
执行期间触发的事件断言统计信息。以下测试演示了如何在 JUnit Jupiter TestEngine
中断言容器和测试的统计信息。有关可用统计信息的详细信息,请查阅 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 Jupiter TestEngine 。 |
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 Jupiter TestEngine 。 |
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(it -> it.endsWith("by zero")))));
}
}
1 | 选择 JUnit Jupiter TestEngine 。 |
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(it -> it.endsWith("by zero")))),
event(container(ExampleTestCase.class), finishedSuccessfully()),
event(engine(), finishedSuccessfully()));
}
}
1 | 选择 JUnit Jupiter TestEngine 。 |
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 的启动基础设施中。
launcher API 在 junit-platform-launcher
模块中。
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
当 LauncherSession
打开时(在 Launcher
首次发现和执行测试之前)和关闭时(当不再发现或执行测试时),将通知 LauncherSessionListener
的已注册实现。可以通过传递给 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
。
例如,在 /META-INF/services/org.junit.platform.launcher.TestExecutionListener
文件中实现的 TestExecutionListener
和声明的 example.CustomTestExecutionListener
类将被自动加载和注册。
6.4.9. 配置 TestExecutionListener
当通过 Launcher
API 以编程方式注册 TestExecutionListener
时,监听器可以提供以编程方式配置它的方法 — 例如,通过其构造函数、setter 方法等。但是,当通过 Java 的 ServiceLoader
机制自动注册 TestExecutionListener
时(请参阅注册 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
的实例是通过内置的流畅 builder 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. Dry-Run 模式
当通过 Launcher
API 运行测试时,您可以通过将 junit.platform.execution.dryRun.enabled
配置参数设置为 true
来启用dry-run 模式。在此模式下,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 团队的 TestEngine 保留JUnit 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 Test Engines 的实现或 JUnit 5 wiki 中列出的第三方测试引擎的实现。 您还可以在互联网上找到各种教程和博客,演示如何编写自定义 TestEngine
。
HierarchicalTestEngine 是 TestEngine SPI(由 junit-jupiter-engine 使用)的便捷抽象基类实现,它仅要求实现者提供测试发现的逻辑。 它实现了 TestDescriptors 的执行,这些 TestDescriptors 实现了 Node 接口,包括对并行执行的支持。 |
6.5.3. 注册 TestEngine
TestEngine
注册通过 Java 的 ServiceLoader
机制支持。
例如,junit-jupiter-engine
模块在其 JAR 中的 /META-INF/services
文件夹中名为 org.junit.platform.engine.TestEngine
的文件中注册了其 org.junit.jupiter.engine.JupiterTestEngine
。
6.5.4. 要求
本节中的“必须 (must)”,“不得 (must not)”,“必需 (required)”,“应 (shall)”,“不应 (shall not)”,“应该 (should)”,“不应该 (should not)”,“推荐 (recommended)”,“可以 (may)”和“可选 (optional)”等词语应按照 RFC 2119 中的描述进行解释。 |
强制性要求
为了与构建工具和 IDE 互操作,TestEngine
实现必须遵守以下要求
-
从
TestEngine.discover()
返回的TestDescriptor
必须 是TestDescriptor
实例树的根。 这意味着在节点及其后代之间不得存在任何循环。 -
TestEngine
必须 能够为先前从TestEngine.discover()
生成和返回的任何唯一 ID 发现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 4 中,许多最初作为内部构造添加的内容仅被外部扩展编写者和工具构建者使用。 这使得更改 JUnit 4 特别困难,有时甚至不可能。
这就是 JUnit 5 为所有公开可用的接口、类和方法引入定义的生命周期的原因。
7.1. API 版本和状态
每个发布的工件都有一个版本号 <major>.<minor>.<patch>
,并且所有公开可用的接口、类和方法都使用来自 @API Guardian 项目的 @API 进行注释。 注释的 status
属性可以分配以下值之一。
状态 | 描述 |
---|---|
|
不得被 JUnit 自身以外的任何代码使用。 可能会在没有事先通知的情况下删除。 |
|
应不再使用; 可能会在下一个小版本中消失。 |
|
旨在用于新的实验性功能,我们在其中寻求反馈。 |
|
旨在用于在当前主要版本的至少下一个小版本中不会以向后不兼容的方式更改的功能。 如果计划删除,它将首先降级为 |
|
旨在用于在当前主要版本 ( |
如果 @API
注释存在于类型上,则认为它也适用于该类型的所有公共成员。 成员可以声明稳定性较低的不同 status
值。
7.2. 实验性 API
下表列出了当前通过 @API(status = EXPERIMENTAL)
指定为实验性的 API。 依赖此类 API 时应谨慎。
包名 | 名称 | 始于 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.3. 废弃的 API
下表列出了当前通过 @API(status = DEPRECATED)
指定为废弃的 API。 您应尽可能避免使用废弃的 API,因为此类 API 可能会在即将发布的版本中删除。
包名 | 名称 | 始于 |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.4. @API 工具支持
@API Guardian 项目计划为使用 @API 注释的 API 的发布者和消费者提供工具支持。 例如,工具支持可能会提供一种方法来检查 JUnit API 的使用是否符合 @API
注释声明。
8. 贡献者
直接在 GitHub 上浏览当前的贡献者列表。
9. 发行说明
发行说明可在此处获取:此处。
10. 附录
10.1. 可重现构建
从 5.7 版本开始,JUnit 5 旨在使其非 javadoc JAR 成为可重现的。
在相同的构建条件下(例如 Java 版本),重复构建应提供相同的逐字节输出。
这意味着任何人都可以重现 Maven Central/Sonatype 上工件的构建条件,并在本地生成相同的输出工件,从而确认存储库中的工件实际上是从此源代码生成的。
10.2. 依赖元数据
最终版本和里程碑版本的工件部署到 Maven Central,快照工件部署到 Sonatype 的 快照存储库,路径为 /org/junit。
以下各节列出了平台 (Platform)、木星 (Jupiter) 和古董 (Vintage) 这三个组的所有工件及其版本:平台、木星 和 古董。物料清单 (BOM) 包含上述所有工件及其版本的列表。
对齐依赖版本
为确保所有 JUnit 工件彼此兼容,它们的版本应保持一致。 如果您依赖 Spring Boot 进行依赖管理,请参阅相应章节。 否则,建议应用 BOM 到您的项目,而不是管理 JUnit 工件的各个版本。 请参阅 Maven 或 Gradle 的相应章节。 |
10.2.1. JUnit 平台
-
组 ID:
org.junit.platform
-
版本:
1.12.0
-
工件 ID:
junit-platform-commons
-
JUnit 平台的通用 API 和支持实用程序。 任何使用
@API(status = INTERNAL)
注释的 API 仅供 JUnit 框架本身内部使用。 不支持外部各方使用任何内部 API! junit-platform-console
-
支持从控制台发现和执行 JUnit 平台上的测试。 有关详细信息,请参阅控制台启动器。
junit-platform-console-standalone
-
Maven Central 的 junit-platform-console-standalone 目录中提供了包含所有依赖项的可执行胖 JAR。 有关详细信息,请参阅控制台启动器。
junit-platform-engine
-
测试引擎的公共 API。 有关详细信息,请参阅注册 TestEngine。
junit-platform-jfr
-
为 JUnit 平台上的 Java Flight Recorder 事件提供
LauncherDiscoveryListener
和TestExecutionListener
。 有关详细信息,请参阅Flight Recorder 支持。 junit-platform-launcher
-
用于配置和启动测试计划的公共 API,通常由 IDE 和构建工具使用。 有关详细信息,请参阅JUnit 平台启动器 API。
junit-platform-reporting
-
生成测试报告的
TestExecutionListener
实现,通常由 IDE 和构建工具使用。 有关详细信息,请参阅JUnit 平台报告。 junit-platform-runner
-
用于在 JUnit 4 环境中执行 JUnit 平台上的测试和测试套件的运行器。 有关详细信息,请参阅使用 JUnit 4 运行 JUnit 平台。
junit-platform-suite
-
JUnit 平台套件工件,它传递性地引入了对
junit-platform-suite-api
和junit-platform-suite-engine
的依赖,以便简化 Gradle 和 Maven 等构建工具中的依赖管理。 junit-platform-suite-api
-
用于在 JUnit 平台上配置测试套件的注解。 由 JUnit 平台套件引擎 和 JUnitPlatform 运行器 支持。
junit-platform-suite-commons
-
用于在 JUnit 平台上执行测试套件的通用支持实用程序。
junit-platform-suite-engine
-
在 JUnit 平台上执行测试套件的引擎; 仅在运行时需要。 有关详细信息,请参阅JUnit 平台套件引擎。
junit-platform-testkit
-
提供对给定
TestEngine
执行测试计划的支持,然后通过流畅的 API 访问结果以验证预期结果。
10.2.2. JUnit 木星
-
组 ID:
org.junit.jupiter
-
版本:
5.12.0
-
工件 ID:
junit-jupiter
-
JUnit 木星聚合器工件,它传递性地引入了对
junit-jupiter-api
、junit-jupiter-params
和junit-jupiter-engine
的依赖,以便简化 Gradle 和 Maven 等构建工具中的依赖管理。 junit-jupiter-api
junit-jupiter-engine
-
JUnit 木星测试引擎实现; 仅在运行时需要。
junit-jupiter-params
-
支持 JUnit 木星中的参数化测试。
junit-jupiter-migrationsupport
-
支持从 JUnit 4 迁移到 JUnit 木星; 仅在支持 JUnit 4 的
@Ignore
注解和运行选定的 JUnit 4 规则时才需要。
10.2.3. JUnit 古董
-
组 ID:
org.junit.vintage
-
版本:
5.12.0
-
工件 ID:
junit-vintage-engine
-
JUnit 古董测试引擎实现,允许在 JUnit 平台上运行古董 JUnit 测试。 古董测试包括使用 JUnit 3 或 JUnit 4 API 编写的测试,或使用基于这些 API 构建的测试框架编写的测试。