1. 概述
本文档的目标是为编写测试的程序员、扩展作者、引擎作者以及构建工具和 IDE 供应商提供全面的参考文档。
该文档还提供PDF 版本下载。
1.1. 什么是 JUnit 5?
与以前版本的 JUnit 不同,JUnit 5 由来自三个不同子项目的多个不同模块组成。
JUnit 5 = JUnit 平台+ JUnit Jupiter + JUnit Vintage
JUnit平台是在 JVM 上启动测试框架的基础。它还定义了TestEngine用于开发在平台上运行的测试框架的API。此外,该平台还提供了一个
控制台启动器,用于从命令行启动平台,还提供JUnit 平台套件引擎,用于使用平台上的一个或多个测试引擎来运行自定义测试套件。对 JUnit 平台的一流支持也存在于流行的 IDE(请参阅IntelliJ IDEA、
Eclipse、NetBeans和
Visual Studio Code)和构建工具(请参阅Gradle、
Maven和Ant)中。
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 的 |
|
用于配置带注释的测试类的测试实例生命周期。此类注释是继承的。 |
|
声明测试类或测试方法的自定义显示名称。此类注释不会被继承。 |
|
为测试类声明一个自定义显示名称生成器。此类注释是继承的。 |
|
表示被注解的方法应该在当前类中的每个, , , 或方法之前执行; 类似于 JUnit 4 的. 此类方法是继承的,除非它们被覆盖或取代(即仅基于签名进行替换,而不考虑 Java 的可见性规则)。 |
|
表示被注解的方法应该在当前类中的每个、、、 或方法之后执行; 类似于 JUnit 4 的. 此类方法是继承的,除非它们被覆盖或取代(即仅基于签名进行替换,而不考虑 Java 的可见性规则)。 |
|
表示被注解的方法应该在当前类中的所有、、、方法之前执行; 类似于 JUnit 4 的. 此类方法是继承的,除非它们被隐藏、重写或取代(即,仅根据签名进行替换,而不考虑 Java 的可见性规则),而且必须如此,除非使用“每类”测试实例生命周期。 |
|
表示被注解的方法应该在当前类中的所有、、、方法之后执行; 类似于 JUnit 4 的. 此类方法是继承的,除非它们被隐藏、重写或取代(即,仅根据签名进行替换,而不考虑 Java 的可见性规则),而且必须如此,除非使用“每类”测试实例生命周期。 |
|
表示被注解的类是一个非静态嵌套测试类。在 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 自动将以下内容识别为@Test带有“快速”标签的方法。
@FastTest
void myFastTest() {
// ...
}
2.3. 测试类和方法
测试方法和生命周期方法可以在当前测试类中本地声明、从超类继承或从接口继承(请参阅
测试接口和默认方法)。此外,测试方法和生命周期方法不能abstract也不能返回值(@TestFactory
需要返回值的方法除外)。
|
类和方法的可见性
测试类、测试方法和生命周期方法不需要是 通常建议省略 |
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class StandardTests {
@BeforeAll
static void initAll() {
}
@BeforeEach
void init() {
}
@Test
void succeedingTest() {
}
@Test
void failingTest() {
fail("a failing test");
}
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}
@AfterEach
void tearDown() {
}
@AfterAll
static void tearDownAll() {
}
}
2.4. 显示名称
测试类和测试方法可以通过@DisplayName 空格、特殊字符甚至表情符号来声明自定义显示名称,这些名称将显示在测试报告中以及测试运行程序和 IDE 中。
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("A special test case")
class DisplayNameDemo {
@Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() {
}
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("????")
void testWithDisplayNameContainingEmoji() {
}
}
2.4.1. 显示名称生成器
JUnit Jupiter 支持可以通过
@DisplayNameGeneration注释进行配置的自定义显示名称生成器。通过注释提供的值@DisplayName始终优先于DisplayNameGenerator.
生成器可以通过实现来创建DisplayNameGenerator。以下是 Jupiter 中可用的一些默认值:
| 显示名称生成器 | 行为 |
|---|---|
|
匹配自 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(如果存在) -
通过调用注释
DisplayNameGenerator中指定的@DisplayNameGeneration(如果存在) -
DisplayNameGenerator通过调用通过配置参数配置的默认值(如果存在) -
通过致电
org.junit.jupiter.api.DisplayNameGenerator.Standard
2.5. 断言
JUnit Jupiter 附带了 JUnit 4 所具有的许多断言方法,并添加了一些非常适合与 Java 8 lambda 一起使用的断言方法。所有 JUnit Jupiter 断言都是类static中的方法org.junit.jupiter.api.Assertions。
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import example.domain.Person;
import example.util.Calculator;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
class AssertionsDemo {
private final Calculator calculator = new Calculator();
private final Person person = new Person("Jane", "Doe");
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and all
// failures will be reported together.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}
@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
new CountDownLatch(1).await();
});
}
private static String greeting() {
return "Hello, World!";
}
}
|
抢先超时
assertTimeoutPreemptively()
一个常见的例子是 Spring 框架中的事务测试支持。 其他依赖
|
2.5.1. Kotlin 断言支持
JUnit Jupiter 还附带了一些非常适合在Kotlin中使用的断言方法。所有 JUnit Jupiter Kotlin 断言都是包中的顶级函数org.junit.jupiter.api。
import example.domain.Person
import example.util.Calculator
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.assertTimeout
import org.junit.jupiter.api.assertTimeoutPreemptively
import java.time.Duration
class KotlinAssertionsDemo {
private val person = Person("Jane", "Doe")
private val people = setOf(person, Person("John", "Doe"))
@Test
fun `exception absence testing`() {
val calculator = Calculator()
val result = assertDoesNotThrow("Should not throw an exception") {
calculator.divide(0, 1)
}
assertEquals(0, result)
}
@Test
fun `expected exception testing`() {
val calculator = Calculator()
val exception = assertThrows<ArithmeticException> ("Should throw an exception") {
calculator.divide(1, 0)
}
assertEquals("/ by zero", exception.message)
}
@Test
fun `grouped assertions`() {
assertAll(
"Person properties",
{ assertEquals("Jane", person.firstName) },
{ assertEquals("Doe", person.lastName) }
)
}
@Test
fun `grouped assertions from a stream`() {
assertAll(
"People with first name starting with J",
people
.stream()
.map {
// This mapping returns Stream<() -> Unit>
{ assertTrue(it.firstName.startsWith("J")) }
}
)
}
@Test
fun `grouped assertions from a collection`() {
assertAll(
"People with last name of Doe",
people.map { { assertEquals("Doe", it.lastName) } }
)
}
@Test
fun `timeout not exceeded testing`() {
val fibonacciCalculator = FibonacciCalculator()
val result = assertTimeout(Duration.ofMillis(1000)) {
fibonacciCalculator.fib(14)
}
assertEquals(377, result)
}
@Test
fun `timeout exceeded with preemptive termination`() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(Duration.ofMillis(10)) {
// Simulate task that takes more than 10 ms.
Thread.sleep(100)
}
}
}
2.5.2. 第三方断言库
尽管 JUnit Jupiter 提供的断言工具足以满足许多测试场景,但有时 需要或需要更多功能和附加功能(例如匹配器) 。在这种情况下,JUnit 团队建议使用第三方断言库,例如AssertJ、Hamcrest、Truth等。因此,开发人员可以自由使用自己选择的断言库。
例如,匹配器和流畅 API 的组合可用于使断言更具描述性和可读性。然而,JUnit Jupiter 的org.junit.jupiter.api.Assertions类不提供
assertThat()
类似于 JUnit 4 的org.junit.Assert类中接受 Hamcrest 的
方法Matcher。相反,鼓励开发人员使用第三方断言库提供的对匹配器的内置支持。
以下示例演示了如何assertThat()在 JUnit Jupiter 测试中使用 Hamcrest 的支持。只要将 Hamcrest 库添加到类路径中,您就可以静态导入assertThat()、is()、 等方法equalTo(),然后在测试中使用它们,如assertWithHamcrestMatcher()下面的方法所示。
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class HamcrestAssertionsDemo {
private final Calculator calculator = new Calculator();
@Test
void assertWithHamcrestMatcher() {
assertThat(calculator.subtract(4, 1), is(equalTo(3)));
}
}
当然,基于 JUnit 4 编程模型的遗留测试可以继续使用
org.junit.Assert#assertThat.
2.6。假设
JUnit Jupiter 附带了 JUnit 4 提供的假设方法的子集,并添加了一些非常适合与 Java 8 lambda 表达式和方法引用一起使用的方法。所有 JUnit Jupiter 假设都是类中的静态方法
org.junit.jupiter.api.Assumptions。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class AssumptionsDemo {
private final Calculator calculator = new Calculator();
@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}
@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
}
@Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// perform these assertions only on the CI server
assertEquals(2, calculator.divide(4, 2));
});
// perform these assertions in all environments
assertEquals(42, calculator.multiply(6, 7));
}
}
从 JUnit Jupiter 5.4 开始,还可以使用 JUnit 4
org.junit.Assume类中的方法进行假设。具体来说,JUnit Jupiter 支持 JUnit 4
AssumptionViolatedException来发出信号,表明测试应该中止,而不是标记为失败。
|
2.7. 禁用测试
整个测试类或单个测试方法可以通过注释、通过条件测试执行@Disabled
中讨论的注释之一
或通过自定义.ExecutionCondition
这是一个@Disabled测试类。
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
这是一个包含@Disabled测试方法的测试类。
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
class DisabledTestsDemo {
@Disabled("Disabled until bug #42 has been resolved")
@Test
void testWillBeSkipped() {
}
@Test
void testWillBeExecuted() {
}
}
|
|
|
|
2.8. 条件测试执行
JUnit Jupiter 中的扩展ExecutionConditionAPI 允许开发人员以编程方式启用或禁用容器或根据某些条件进行测试。这种情况最简单的例子是
支持注释的内置函数(请参阅
禁用测试)。除此之外,JUnit Jupiter 还在包中支持其他几种基于注释的条件
,允许开发人员以声明方式启用或禁用容器和测试。当注册多个扩展时,一旦其中一个条件返回disabled ,容器或测试就会被禁用。如果您希望提供有关为何禁用它们的详细信息,与这些内置条件关联的每个注释都有一个可用于该目的的属性。DisabledCondition@Disabled@Disabledorg.junit.jupiter.api.conditionExecutionConditiondisabledReason
有关详细信息,请参阅ExecutionCondition和以下部分。
|
组合注释
请注意,以下部分中列出的任何条件注释也可以用作元注释,以便创建自定义组合注释。例如,@EnabledOnOs 演示 |
|
JUnit Jupiter 中的条件注释不是 |
|
除非另有说明,否则以下部分中列出的每个条件注释只能在给定的测试接口、测试类或测试方法上声明一次。如果条件注释在给定元素上直接存在、间接存在或元存在多次,则仅使用 JUnit 发现的第一个此类注释;任何额外的声明都将被默默地忽略。但请注意,每个条件注释可以与 |
2.8.1. 操作系统和架构条件
@EnabledOnOs可以通过和注释在特定操作系统、体系结构或两者的组合上启用或禁用容器或测试@DisabledOnOs
。
@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
// ...
}
@TestOnMac
void testOnMac() {
// ...
}
@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
// ...
}
@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
// ...
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}
@Test
@EnabledOnOs(architectures = "aarch64")
void onAarch64() {
// ...
}
@Test
@DisabledOnOs(architectures = "x86_64")
void notOnX86_64() {
// ...
}
@Test
@EnabledOnOs(value = MAC, architectures = "aarch64")
void onNewMacs() {
// ...
}
@Test
@DisabledOnOs(value = MAC, architectures = "aarch64")
void notOnNewMacs() {
// ...
}
2.8.2. Java运行环境条件
@EnabledOnJre可以通过和注释在特定版本的 Java 运行时环境 (JRE) 上启用或禁用容器或测试,或者通过和
注释@DisabledOnJre在特定版本的 JRE 上启用或禁用容器或测试。该范围默认为下边界 ( ) 和上边界 ( ),这允许使用半开范围。@EnabledForJreRange@DisabledForJreRangeJRE.JAVA_8minJRE.OTHERmax
@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
// ...
}
@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
// ...
}
@Test
@EnabledForJreRange(min = JAVA_9)
void fromJava9toCurrentJavaFeatureNumber() {
// ...
}
@Test
@EnabledForJreRange(max = JAVA_11)
void fromJava8To11() {
// ...
}
@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
// ...
}
@Test
@DisabledForJreRange(min = JAVA_9, max = JAVA_11)
void notFromJava9to11() {
// ...
}
@Test
@DisabledForJreRange(min = JAVA_9)
void notFromJava9toCurrentJavaFeatureNumber() {
// ...
}
@Test
@DisabledForJreRange(max = JAVA_11)
void notFromJava8to11() {
// ...
}
2.8.3. 原生图像条件
可以通过
和注释在GraalVM 本机映像中启用或禁用容器或测试
。当使用 GraalVM Native Build Tools项目中的 Gradle 和 Maven 插件在本机映像中运行测试时,通常会使用这些注释。@EnabledInNativeImage@DisabledInNativeImage
@Test
@EnabledInNativeImage
void onlyWithinNativeImage() {
// ...
}
@Test
@DisabledInNativeImage
void neverWithinNativeImage() {
// ...
}
2.8.4. 系统属性条件
named可以通过@EnabledIfSystemProperty和注释根据 JVM 系统属性的值启用或禁用容器或测试@DisabledIfSystemProperty
。通过属性提供的值matches将被解释为正则表达式。
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
// ...
}
@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
// ...
}
|
从 JUnit Jupiter 5.6 开始, |
2.8.5。环境变化条件
可以通过和注释根据named
来自底层操作系统的环境变量
值来启用或禁用容器或测试。通过属性提供的值将被解释为正则表达式。@EnabledIfEnvironmentVariable@DisabledIfEnvironmentVariablematches
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
// ...
}
@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
// ...
}
|
从 JUnit Jupiter 5.6 开始, |
2.8.6。定制条件
作为实现 的替代方案,可以根据通过和注释配置的条件方法ExecutionCondition来启用或禁用容器或测试。条件方法必须具有
返回类型,并且可以不接受参数或接受单个参数。@EnabledIf@DisabledIfbooleanExtensionContext
以下测试类演示了如何配置名为
customConditionvia@EnabledIf和的本地方法@DisabledIf。
@Test
@EnabledIf("customCondition")
void enabled() {
// ...
}
@Test
@DisabledIf("customCondition")
void disabled() {
// ...
}
boolean customCondition() {
return true;
}
或者,条件方法可以位于测试类之外。在这种情况下,必须通过其完全限定名称来引用它,如以下示例所示。
package example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
class ExternalCustomConditionDemo {
@Test
@EnabledIf("example.ExternalCondition#customCondition")
void enabled() {
// ...
}
}
class ExternalCondition {
static boolean customCondition() {
return true;
}
}
|
在多种情况下,需要使用条件方法
在任何其他情况下,您可以使用静态方法或实例方法作为条件方法。 |
|
通常情况下,您可以使用实用程序类中的现有静态方法作为自定义条件。 例如,
|
2.9. 标记和过滤
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
@Tag("model")
class TaggingDemo {
@Test
@Tag("taxes")
void testingTaxCalculation() {
}
}
| 有关演示如何为标签创建自定义注释的示例, 请参阅元注释和组合注释。 |
2.10. 测试执行顺序
默认情况下,测试类和方法将使用确定性但故意不明显的算法进行排序。这确保了测试套件的后续运行以相同的顺序执行测试类和测试方法,从而允许可重复的构建。
| 有关测试方法和测试类的定义, 请参阅定义。 |
2.10.1. 方法顺序
虽然真正的unit测试通常不应该依赖于它们的执行顺序,但有时有必要强制执行特定的测试方法执行顺序 - 例如,在编写集成测试或功能测试时,测试的顺序是很重要,尤其是与
@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默认使用 orderer 方法,您应该将配置参数设置为相应的完全限定类名(例如,在 中src/test/resources/junit-platform.properties):
junit.jupiter.testmethod.order.default = \
org.junit.jupiter.api.MethodOrderer$OrderAnnotation
同样,您可以指定任何实现
MethodOrderer.
2.10.2. 班级顺序
尽管测试类通常不应依赖于它们的执行顺序,但有时需要强制执行特定的测试类执行顺序。您可能希望以随机顺序执行测试类,以确保测试类之间不存在意外的依赖关系,或者您可能希望对测试类进行排序以优化构建时间,如以下场景中所述。
-
首先运行之前失败的测试和更快的测试:“快速失败”模式
-
启用并行执行后,首先安排较长的测试:“最短测试计划执行持续时间”模式
-
各种其他用例
要为整个测试套件全局配置测试类执行顺序,请使用
junit.jupiter.testclass.order.default 配置参数ClassOrderer指定您要使用的完全限定类名称。提供的类必须实现该ClassOrderer接口。
您可以实现自己的自定义ClassOrderer或使用以下内置
ClassOrderer实现之一。
-
ClassOrderer.ClassName:根据测试类的完全限定类名按字母数字顺序对测试类进行排序 -
ClassOrderer.DisplayName:根据显示名称按字母数字顺序对测试类进行排序(请参阅显示名称生成优先规则) -
ClassOrderer.OrderAnnotation:根据通过注释指定的值对测试类进行数字排序@Order -
ClassOrderer.Random:伪随机地订购测试类并支持自定义种子的配置
例如,为了在测试类@Order上遵循注释,您应该使用配置参数和相应的完全限定类名(例如,在 中
)来配置类排序器:ClassOrderer.OrderAnnotationsrc/test/resources/junit-platform.properties
junit.jupiter.testclass.order.default = \
org.junit.jupiter.api.ClassOrderer$OrderAnnotation
配置的内容ClassOrderer将应用于所有顶级测试类(包括
static嵌套测试类)和@Nested测试类。
顶级测试类将相对于彼此进行排序;然而,@Nested
测试类将相对于@Nested共享同一
封闭类的其他测试类进行排序。
|
要在本地为测试类配置测试类执行顺序@Nested,请
在要排序的测试类@TestClassOrder的封闭类上声明注释,并提供对要在注释中直接使用的实现的@Nested类引用。配置的内容
将递归地应用于测试类及其测试类。请注意,本地声明始终会覆盖继承的
声明或通过
配置参数全局配置的声明。ClassOrderer@TestClassOrderClassOrderer@Nested@Nested@TestClassOrder@TestClassOrderClassOrdererjunit.jupiter.testclass.order.default
下面的示例演示了如何保证@Nested测试类按照@Order注释指定的顺序执行。
import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class OrderedNestedTestClassesDemo {
@Nested
@Order(1)
class PrimaryTests {
@Test
void test1() {
}
}
@Nested
@Order(2)
class SecondaryTests {
@Test
void test2() {
}
}
}
2.11. 测试实例生命周期
为了允许单独执行各个测试方法并避免由于可变测试实例状态而导致意外的副作用,JUnit 在执行每个测试方法之前创建每个测试类的新实例(请参阅定义 )。这种“按方法”测试实例生命周期是 JUnit Jupiter 中的默认行为,类似于所有以前版本的 JUnit。
请注意,如果通过条件(例如,,
等)
禁用给定的测试方法,即使“按方法”测试实例生命周期模式处于活动状态,测试类仍将被实例化。
@Disabled@DisabledOnOs |
如果您希望 JUnit Jupiter 在同一测试实例上执行所有测试方法,请使用 注释您的测试类@TestInstance(Lifecycle.PER_CLASS)。使用此模式时,每个测试类都会创建一个新的测试实例。因此,如果您的测试方法依赖于存储在实例变量中的状态,您可能需要在
@BeforeEach或@AfterEach方法中重置该状态。
与默认的“每个方法”模式相比,“每个类”模式有一些额外的好处。具体来说,通过“每类”模式,可以在非静态方法和接口方法上声明@BeforeAll和
。因此,“每类”模式也使得在测试类中使用和
方法成为可能。@AfterAlldefault@BeforeAll@AfterAll@Nested
从 Java 16 开始,@BeforeAll可以像在测试类中@AfterAll一样声明方法
。
static@Nested |
如果您使用 Kotlin 编程语言编写测试,您可能还会发现通过切换到“每类”测试实例生命周期模式,可以更轻松地实现非静态@BeforeAll和@AfterAll生命周期方法以及
工厂方法。@MethodSource
2.11.1. 更改默认测试实例生命周期
如果测试类或测试接口没有使用 注释@TestInstance,JUnit Jupiter 将使用默认的生命周期模式。标准默认模式是PER_METHOD;但是,可以更改整个测试计划的执行默认值。要更改默认测试实例生命周期模式,请将
junit.jupiter.testinstance.lifecycle.default 配置参数设置为 中定义的枚举常量的名称TestInstance.Lifecycle,忽略大小写。这可以作为 JVM 系统属性、作为传递到 的配置
参数提供,或者通过 JUnit 平台配置文件提供(有关详细信息,请参阅配置参数)。LauncherDiscoveryRequestLauncher
例如,要将默认测试实例生命周期模式设置为Lifecycle.PER_CLASS,您可以使用以下系统属性启动 JVM。
-Djunit.jupiter.testinstance.lifecycle.default=per_class
但请注意,通过 JUnit 平台配置文件设置默认测试实例生命周期模式是一种更可靠的解决方案,因为配置文件可以与项目一起检入版本控制系统,因此可以在 IDE 和构建软件中使用。
要将默认测试实例生命周期模式设置为通过 JUnit Platform 配置文件,请使用以下内容在类路径的根目录中Lifecycle.PER_CLASS创建一个名为的文件(例如 )。junit-platform.propertiessrc/test/resources
junit.jupiter.testinstance.lifecycle.default = per_class
| 如果不一致应用,更改默认测试实例生命周期模式可能会导致不可预测的结果和脆弱的构建。例如,如果构建将“每类”语义配置为默认值,但 IDE 中的测试是使用“每方法”语义执行的,则可能会导致调试构建服务器上发生的错误变得困难。因此,建议更改 JUnit 平台配置文件中的默认值,而不是通过 JVM 系统属性。 |
2.12. 嵌套测试
@Nested测试为测试编写者提供了更多能力来表达多组测试之间的关系。此类嵌套测试利用 Java 的嵌套类并促进对测试结构的分层思考。下面是一个详细的示例,既包含源代码,也包含 IDE 中执行的屏幕截图。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
在 IDE 中执行此示例时,GUI 中的测试执行树将类似于下图。
在此示例中,通过为设置代码定义分层生命周期方法,在内部测试中使用外部测试的前提条件。例如,createNewStack()是一个
@BeforeEach生命周期方法,用在定义它的测试类中以及定义它的类下面的嵌套树的所有级别中。
事实上,外部测试的设置代码在执行内部测试之前运行,使您能够独立运行所有测试。您甚至可以单独运行内部测试而不运行外部测试,因为外部测试中的设置代码始终会被执行。
只有非静态嵌套类(即内部类)才能作为@Nested测试类。嵌套可以是任意深度,并且这些内部类受到完整生命周期支持,但有一个例外:@BeforeAll并且方法默认情况下@AfterAll不起作用。原因是 Java 不允许在 Java 16 之前的内部类中使用成员。但是,可以通过使用注释测试类来规避此限制(请参阅
测试实例生命周期)。如果您使用的是 Java 16 或更高版本,
并且可以像在测试类中一样声明方法,则此限制不再适用。
static@Nested@TestInstance(Lifecycle.PER_CLASS)@BeforeAll@AfterAllstatic@Nested |
2.13. 构造函数和方法的依赖注入
在所有以前的 JUnit 版本中,测试构造函数或方法不允许有参数(至少在标准Runner实现中不允许)。作为 JUnit Jupiter 的主要变化之一,测试构造函数和方法现在都允许具有参数。这提供了更大的灵活性,并为构造函数和方法启用依赖注入。
ParameterResolver为希望在运行时动态
解析参数的测试扩展定义 API。如果测试类构造函数、测试方法或
生命周期方法(请参阅定义)接受参数,则该参数必须在运行时由注册的ParameterResolver.
目前有三个自动注册的内置解析器。
-
TestInfoParameterResolver:如果构造函数或方法参数的类型为TestInfo,则将TestInfoParameterResolver提供与当前容器或测试相对应的实例TestInfo作为参数的值。然后TestInfo可用于检索有关当前容器或测试的信息,例如显示名称、测试类、测试方法和关联标签。显示名称可以是技术名称,例如测试类或测试方法的名称,也可以是通过配置的自定义名称@DisplayName。TestInfo充当 JUnit 4 中规则的直接替换。TestName以下演示了如何TestInfo注入测试构造函数、@BeforeEach方法和@Test方法。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
@DisplayName("TestInfo Demo")
class TestInfoDemo {
TestInfoDemo(TestInfo testInfo) {
assertEquals("TestInfo Demo", testInfo.getDisplayName());
}
@BeforeEach
void init(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}
@Test
@DisplayName("TEST 1")
@Tag("my-tag")
void test1(TestInfo testInfo) {
assertEquals("TEST 1", testInfo.getDisplayName());
assertTrue(testInfo.getTags().contains("my-tag"));
}
@Test
void test2() {
}
}
-
RepetitionExtension@RepeatedTest:如果、@BeforeEach、 或 方法中的方法参数@AfterEach属于 类型RepetitionInfo,则将RepetitionExtension提供 的实例RepetitionInfo。RepetitionInfo然后可用于检索有关当前重复、重复总数、失败的重复次数以及相应 的失败阈值的信息@RepeatedTest。但请注意,它RepetitionExtension不会在 的上下文之外注册@RepeatedTest。请参阅重复测试示例。 -
TestReporterParameterResolver:如果构造函数或方法参数的类型为TestReporter,则将TestReporterParameterResolver提供 的实例TestReporter。可TestReporter用于发布有关当前测试运行的附加数据。reportingEntryPublished()可以通过a 中的方法使用数据TestExecutionListener,从而可以在 IDE 中查看数据或将其包含在报告中。在 JUnit Jupiter 中,您应该使用在 JUnit 4 中
TestReporter打印信息的位置。stdout使用会将所有报告的条目输出到. 此外,某些 IDE 还会将报告条目打印到或显示在用户界面中以获取测试结果。stderr@RunWith(JUnitPlatform.class)stdoutstdout
class TestReporterDemo {
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a status message");
}
@Test
void reportKeyValuePair(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
@Test
void reportMultipleKeyValuePairs(TestReporter testReporter) {
Map<String, String> values = new HashMap<>();
values.put("user name", "dk38");
values.put("award year", "1974");
testReporter.publishEntry(values);
}
}
其他参数解析器必须通过注册适当的
扩展来@ExtendWith显式启用。
|
查看RandomParametersExtension的 自定义示例
ParameterResolver。虽然不打算投入生产,但它展示了扩展模型和参数解析过程的简单性和表现力。MyRandomParametersTest演示如何将随机值注入到@Test
方法中。
@ExtendWith(RandomParametersExtension.class)
class MyRandomParametersTest {
@Test
void injectsInteger(@Random int i, @Random int j) {
assertNotEquals(i, j);
}
@Test
void injectsDouble(@Random double d) {
assertEquals(0.0, d, 1.0);
}
}
对于实际用例,请查看MockitoExtension和 的
源代码SpringExtension。
当要注入的参数类型是您的唯一条件时
ParameterResolver,您可以使用通用TypeBasedParameterResolver基类。该supportsParameters方法在幕后实现并支持参数化类型。
2.14. 测试接口和默认方法
JUnit Jupiter 允许在接口方法上声明@Test、@RepeatedTest、@ParameterizedTest、@TestFactory、
@TestTemplate、 、@BeforeEach和
。如果测试接口或测试类带有注释,则可以在测试接口中的方法或接口方法上声明(请参阅
测试实例生命周期)。这里有些例子。@AfterEachdefault@BeforeAll@AfterAllstaticdefault@TestInstance(Lifecycle.PER_CLASS)
@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {
static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());
@BeforeAll
default void beforeAllTests() {
logger.info("Before all tests");
}
@AfterAll
default void afterAllTests() {
logger.info("After all tests");
}
@BeforeEach
default void beforeEachTest(TestInfo testInfo) {
logger.info(() -> String.format("About to execute [%s]",
testInfo.getDisplayName()));
}
@AfterEach
default void afterEachTest(TestInfo testInfo) {
logger.info(() -> String.format("Finished executing [%s]",
testInfo.getDisplayName()));
}
}
interface TestInterfaceDynamicTestsDemo {
@TestFactory
default Stream<DynamicTest> dynamicTestsForPalindromes() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
}
@ExtendWith并且@Tag可以在测试接口上声明,以便实现该接口的类自动继承其标签和扩展。有关TimingExtension的源代码,
请参阅
测试执行回调之前和之后。
@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}
然后,您可以在您的测试类中实现这些测试接口以应用它们。
class TestInterfaceDemo implements TestLifecycleLogger,
TimeExecutionLogger, TestInterfaceDynamicTestsDemo {
@Test
void isEqualValue() {
assertEquals(1, "a".length(), "is always equal");
}
}
运行TestInterfaceDemo输出类似于以下内容:
INFO example.TestLifecycleLogger - Before all tests INFO example.TestLifecycleLogger - About to execute [dynamicTestsForPalindromes()] INFO example.TimingExtension - Method [dynamicTestsForPalindromes] took 19 ms. INFO example.TestLifecycleLogger - Finished executing [dynamicTestsForPalindromes()] INFO example.TestLifecycleLogger - About to execute [isEqualValue()] INFO example.TimingExtension - Method [isEqualValue] took 1 ms. INFO example.TestLifecycleLogger - Finished executing [isEqualValue()] INFO example.TestLifecycleLogger - After all tests
此功能的另一个可能的应用是为接口契约编写测试。例如,您可以编写测试来了解Object.equals或
的实现Comparable.compareTo应如何表现,如下所示。
public interface Testable<T> {
T createValue();
}
public interface EqualsContract<T> extends Testable<T> {
T createNotEqualValue();
@Test
default void valueEqualsItself() {
T value = createValue();
assertEquals(value, value);
}
@Test
default void valueDoesNotEqualNull() {
T value = createValue();
assertFalse(value.equals(null));
}
@Test
default void valueDoesNotEqualDifferentValue() {
T value = createValue();
T differentValue = createNotEqualValue();
assertNotEquals(value, differentValue);
assertNotEquals(differentValue, value);
}
}
public interface ComparableContract<T extends Comparable<T>> extends Testable<T> {
T createSmallerValue();
@Test
default void returnsZeroWhenComparedToItself() {
T value = createValue();
assertEquals(0, value.compareTo(value));
}
@Test
default void returnsPositiveNumberWhenComparedToSmallerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(value.compareTo(smallerValue) > 0);
}
@Test
default void returnsNegativeNumberWhenComparedToLargerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(smallerValue.compareTo(value) < 0);
}
}
然后,您可以在测试类中实现两个合约接口,从而继承相应的测试。当然,您必须实现抽象方法。
class StringTests implements ComparableContract<String>, EqualsContract<String> {
@Override
public String createValue() {
return "banana";
}
@Override
public String createSmallerValue() {
return "apple"; // 'a' < 'b' in "banana"
}
@Override
public String createNotEqualValue() {
return "cherry";
}
}
| 上述测试仅作为示例,因此并不完整。 |
2.15。反复测试
@RepeatedTestJUnit Jupiter 通过注释方法并指定所需的重复总数,提供了重复测试指定次数的能力。每次调用重复测试的行为就像执行常规
@Test方法一样,完全支持相同的生命周期回调和扩展。
以下示例演示如何声明一个名为的测试,repeatedTest()该测试将自动重复 10 次。
@RepeatedTest(10)
void repeatedTest() {
// ...
}
从 JUnit Jupiter 5.10 开始,@RepeatedTest可以配置一个失败阈值,该阈值表示失败次数,在此之后剩余的重复将被自动跳过。将该failureThreshold属性设置为小于重复总数的正数,以便在遇到指定的失败次数后跳过剩余重复的调用。
例如,如果您经常重复调用您怀疑不稳定的@RepeatedTest测试,则一次失败就足以证明该测试不稳定,并且无需调用其余的重复。要支持该特定用例,请设置. 您也可以根据您的用例将阈值设置为大于 1 的数字。failureThreshold = 1
默认情况下,该failureThreshold属性设置为Integer.MAX_VALUE,表示不会应用失败阈值,这实际上意味着无论是否有任何重复失败,都将调用指定的重复次数。
如果@RepeatedTest方法的重复并行执行,则无法保证失败阈值。因此,建议
在配置并行执行时@RepeatedTest对方法进行注释。@Execution(SAME_THREAD)有关更多详细信息,请参阅并行执行。
|
除了指定重复次数和失败阈值之外,还可以通过注释name的属性
为每次重复配置自定义显示名称@RepeatedTest。此外,显示名称可以是由静态文本和动态占位符的组合组成的模式。目前支持以下占位符。
-
{displayName}:@RepeatedTest方法的显示名称 -
{currentRepetition}:当前的重复次数 -
{totalRepetitions}: 总重复次数
给定重复的默认显示名称是根据以下模式生成的:"repetition {currentRepetition} of {totalRepetitions}"。因此,上一个示例的各个重复的显示名称repeatedTest()将为:
repetition 1 of 10、repetition 2 of 10等。如果您希望@RepeatedTest方法的显示名称包含在每个重复的名称中,您可以定义自己的自定义模式或使用预定义模式RepeatedTest.LONG_DISPLAY_NAME。后者等于"{displayName} :: repetition {currentRepetition} of
{totalRepetitions}"导致单个重复的显示名称,例如
repeatedTest() :: repetition 1 of 10,repeatedTest() :: repetition 2 of 10等。
为了检索有关当前重复、重复总数、失败的重复次数以及失败阈值的信息,开发人员可以选择将实例注入到 、RepetitionInfo或
@RepeatedTest方法@BeforeEach中@AfterEach。
2.15.1. 重复测试实例
RepeatedTestsDemo本节末尾的课程演示了几个重复测试的示例。
该repeatedTest()方法与上一节的示例相同;然而,
repeatedTestWithRepetitionInfo()演示了如何将实例
RepetitionInfo注入到测试中以访问当前重复测试的重复总数。
repeatedTestWithFailureThreshold()演示如何设置故障阈值并模拟每秒重复的意外故障。ConsoleLauncher可以在本节末尾的输出中查看生成的行为。
接下来的两个方法演示了如何在每个重复的显示名称中包含方法@DisplayName的
自定义。
将自定义显示名称与自定义模式相结合,然后用于验证生成的显示名称的格式。是来自声明的 ,并且来自
。相反,
使用前面提到的预定义
模式。@RepeatedTestcustomDisplayName()TestInfoRepeat!{displayName}@DisplayName1/1{currentRepetition}/{totalRepetitions}customDisplayNameWithLongPattern()RepeatedTest.LONG_DISPLAY_NAME
repeatedTestInGerman()演示将重复测试的显示名称翻译成外语的能力 - 在本例中是德语,从而产生单独重复的名称,例如: 、Wiederholung 1 von 5等Wiederholung 2 von 5。
由于该beforeEach()方法带有注释,@BeforeEach因此它将在每个重复测试的每次重复之前执行。通过将TestInfo和
RepetitionInfo注入到该方法中,我们发现可以获得有关当前正在执行的重复测试的信息。RepeatedTestsDemo
在启用日志级别的情况下执行会INFO产生以下输出。
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() {
// ...
}
}
ConsoleLauncher在启用 unicode 主题的情况
下使用时,执行RepeatedTestsDemo结果会在控制台输出以下结果。
├─ RepeatedTestsDemo ✔ │ ├─ repeatedTest() ✔ │ │ ├─ repetition 1 of 10 ✔ │ │ ├─ repetition 2 of 10 ✔ │ │ ├─ repetition 3 of 10 ✔ │ │ ├─ repetition 4 of 10 ✔ │ │ ├─ repetition 5 of 10 ✔ │ │ ├─ repetition 6 of 10 ✔ │ │ ├─ repetition 7 of 10 ✔ │ │ ├─ repetition 8 of 10 ✔ │ │ ├─ repetition 9 of 10 ✔ │ │ └─ repetition 10 of 10 ✔ │ ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔ │ │ ├─ repetition 1 of 5 ✔ │ │ ├─ repetition 2 of 5 ✔ │ │ ├─ repetition 3 of 5 ✔ │ │ ├─ repetition 4 of 5 ✔ │ │ └─ repetition 5 of 5 ✔ │ ├─ repeatedTestWithFailureThreshold(RepetitionInfo) ✔ │ │ ├─ repetition 1 of 8 ✔ │ │ ├─ repetition 2 of 8 ✘ Boom! │ │ ├─ repetition 3 of 8 ✔ │ │ ├─ repetition 4 of 8 ✘ Boom! │ │ ├─ repetition 5 of 8 ↷ Failure threshold [2] exceeded │ │ ├─ repetition 6 of 8 ↷ Failure threshold [2] exceeded │ │ ├─ repetition 7 of 8 ↷ Failure threshold [2] exceeded │ │ └─ repetition 8 of 8 ↷ Failure threshold [2] exceeded │ ├─ Repeat! ✔ │ │ └─ Repeat! 1/1 ✔ │ ├─ Details... ✔ │ │ └─ Details... :: repetition 1 of 1 ✔ │ └─ repeatedTestInGerman() ✔ │ ├─ Wiederholung 1 von 5 ✔ │ ├─ Wiederholung 2 von 5 ✔ │ ├─ Wiederholung 3 von 5 ✔ │ ├─ Wiederholung 4 von 5 ✔ │ └─ Wiederholung 5 von 5 ✔
2.16。参数化测试
参数化测试使得可以使用不同的参数多次运行测试。@Test它们的声明方式与常规方法
一样,但使用@ParameterizedTest注释。此外,您必须声明至少一个
源,该源将为每次调用提供参数,然后在测试方法中使用这些参数。
以下示例演示了一个参数化测试,该@ValueSource
测试使用注释指定String数组作为参数源。
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
执行上述参数化测试方法时,每次调用都会单独报告。例如,ConsoleLauncher将打印类似于以下内容的输出。
palindromes(String) ✔ ├─ [1] candidate=racecar ✔ ├─ [2] candidate=radar ✔ └─ [3] candidate=able was I ere I saw elba ✔
2.16.1. 所需设置
为了使用参数化测试,您需要添加对
junit-jupiter-params工件的依赖项。详细信息请参阅依赖元数据。
2.16.2. 消耗参数
参数化测试方法通常直接使用来自配置源的参数(请参阅参数源),并遵循参数源索引和方法参数索引之间的一对一关联(请参阅
@CsvSource中的示例)。但是,参数化测试方法也可以选择将来自源的参数聚合到传递给该方法的单个对象中(请参阅参数聚合)。还可以通过 a 提供其他参数(例如,ParameterResolver获取 、 等的实例)。具体来说,参数化测试方法必须根据以下规则声明形式参数。TestInfoTestReporter
-
必须首先声明零个或多个索引参数。
-
接下来必须声明零个或多个聚合器。
-
a 提供的零个或多个参数
ParameterResolver必须最后声明。
在此上下文中,索引参数Arguments是由 提供的给定索引的参数,
ArgumentsProvider该参数作为参数传递到方法形式参数列表中相同索引处的参数化方法。聚合
器是任何类型的参数ArgumentsAccessor或任何用 注释的参数
@AggregateWith。
|
自动关闭参数
在为当前参数化测试调用调用方法和
扩展后,实现 为了防止这种情况发生,请将 |
2.16.3。论据来源
JUnit Jupiter 开箱即用地提供了相当多的源注释。以下每个小节都提供了简要概述和示例。请参阅包中的 Javadocorg.junit.jupiter.params.provider了解更多信息。
@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接受单个参数的参数化测试的源和空值。
-
@NullSourcenull:为带注释的方法提供单个参数@ParameterizedTest。-
@NullSource不能用于具有基本类型的参数。
-
-
@EmptySource:为以下类型的参数的带注释方法 提供单个空参数: , (以及具有无参数构造 函数的具体子类型)、 、、、、 (以及具有无参数构造 函数的具体子类型)、 、、 原始数组(例如, , 等),对象数组(例如,,等)。@ParameterizedTestjava.lang.Stringjava.util.Collectionpublicjava.util.Listjava.util.Setjava.util.SortedSetjava.util.NavigableSetjava.util.Mappublicjava.util.SortedMapjava.util.NavigableMapint[]char[][]String[]Integer[][] -
@NullAndEmptySource:组合注释@NullSource,结合了和的功能@EmptySource。
如果您需要向参数化测试提供多个不同类型的空白字符串,则可以使用@ValueSource来实现 - 例如,@ValueSource(strings = {" ", " ", "\t", "\n"}).
您还可以组合@NullSource、@EmptySource、 和@ValueSource来测试更广泛的null、empty和空白输入。以下示例演示了如何对字符串实现此目的。
@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)都会导致 6 次调用:1 次调用null,1 次调用空字符串,4 次调用通过 提供的显式空白字符串@ValueSource。
|
@EnumSource
@EnumSource提供了一种使用Enum常量的便捷方法。
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
注释的value属性是可选的。省略时,使用第一个方法参数的声明类型。如果未引用枚举类型,测试将失败。因此,value在上面的示例中,该属性是必需的,因为方法参数被声明为TemporalUnit,即由 实现的接口ChronoUnit,它不是枚举类型。将方法参数类型更改为ChronoUnit允许您从注释中省略显式枚举类型,如下所示。
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
assertNotNull(unit);
}
该注释提供了一个可选names属性,可让您指定应使用哪些常量,如以下示例所示。如果省略,将使用所有常量。
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}
该@EnumSource注释还提供了一个可选mode属性,可以对传递给测试方法的常量进行细粒度控制。例如,您可以从枚举常量池中排除名称或指定正则表达式,如以下示例所示。
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}
@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
assertTrue(unit.name().endsWith("DAYS"));
}
@方法源
@MethodSource允许您引用测试类或外部类的一个或多个工厂方法。
测试类中的工厂方法必须是,static除非测试类用@TestInstance(Lifecycle.PER_CLASS);注释。然而,外部类中的工厂方法必须始终是static.
每个工厂方法必须生成一个参数流,并且流中的每组参数都将作为带注释方法的单独调用的物理参数提供@ParameterizedTest。一般来说,这会转化为 a
Streamof Arguments(即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");
}
}
工厂方法可以声明参数,这些参数将由扩展 API 的注册实现提供ParameterResolver。在下面的示例中,工厂方法通过其名称进行引用,因为测试类中只有一个这样的方法。如果有多个同名的本地方法,也可以提供参数来区分它们——例如,@MethodSource("factoryMethod()")或
@MethodSource("factoryMethod(java.lang.String)")。或者,工厂方法可以通过其完全限定方法名称来引用,例如
@MethodSource("example.MyTests#factoryMethod(java.lang.String)")。
@RegisterExtension
static final IntegerResolver integerResolver = new IntegerResolver();
@ParameterizedTest
@MethodSource("factoryMethodWithArguments")
void testWithFactoryMethodWithArguments(String argument) {
assertTrue(argument.startsWith("2"));
}
static Stream<Arguments> factoryMethodWithArguments(int quantity) {
return Stream.of(
arguments(quantity + " apples"),
arguments(quantity + " lemons")
);
}
static class IntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().getType() == int.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return 2;
}
}
@CsvSource
@CsvSource允许您将参数列表表示为逗号分隔值(即 CSV
String文字)。value通过in 属性提供的每个字符串@CsvSource
代表一条 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'上例和下表中的值。
除非设置了该属性,否则带引号的空值 ( '') 会导致空值;而完全空的值会被解释为引用。通过指定一个或多个,可以将自定义值解释为参考(请参见下表中的示例)。如果引用的目标类型是原始类型,则会抛出异常
。StringemptyValuenullnullValuesnullNILArgumentConversionExceptionnull
无论通过属性配置任何自定义值,不带引号
的空值都将始终转换为引用。
nullnullValues |
除带引号的字符串外,默认情况下会修剪 CSV 列中的前导和尾随空格。ignoreLeadingAndTrailingWhitespace可以通过将该属性设置为 来更改此行为
true。
| 输入示例 | 结果参数列表 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
如果您使用的编程语言支持文本块 (例如 Java SE 15 或更高版本),您也可以textBlock使用@CsvSource. 文本块中的每条记录代表一条 CSV 记录,并导致参数化测试的一次调用。useHeadersInDisplayName通过将属性设置为,可以选择使用第一条记录来提供 CSV 标头,true如下例所示。
使用文本块,前面的示例可以按如下方式实现。
@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"上面示例中的值。除非
设置了该属性,否则带引号的空值 ( "") 会导致空值;而完全空的值会被解释为
引用。通过指定一个或多个,可以将自定义值解释为参考。如果引用的目标类型是原始类型,则会抛出异常。StringemptyValuenullnullValuesnullArgumentConversionExceptionnull
无论通过属性配置任何自定义值,不带引号
的空值都将始终转换为引用。
nullnullValues |
除带引号的字符串外,默认情况下会修剪 CSV 列中的前导和尾随空格。ignoreLeadingAndTrailingWhitespace可以通过将该属性设置为 来更改此行为
true。
@ArgumentsSource
@ArgumentsSource可用于指定自定义的、可重用的ArgumentsProvider. 请注意, 的实现ArgumentsProvider必须声明为顶级类或static嵌套类。
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}
如果您希望实现ArgumentsProvider也使用注释的自定义(如内置提供程序,如ValueArgumentsProvider或CsvArgumentsProvider),您可以扩展该类AnnotationBasedArgumentsProvider。
2.16.4。参数转换
扩大转换
JUnit Jupiter 支持
对提供给@ParameterizedTest. 例如,用 注释的参数化测试@ValueSource(ints = { 1, 2, 3 })可以声明为不仅接受 类型的参数int,还接受 、 或 类型long的float参数double。
隐式转换
为了支持类似的用例@CsvSource,JUnit Jupiter 提供了许多内置的隐式类型转换器。转换过程取决于每个方法参数的声明类型。
例如,如果 a@ParameterizedTest声明了一个类型的参数TimeUnit,并且声明的源提供的实际类型是 a String,则该字符串将自动转换为相应的TimeUnit枚举常量。
@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(ChronoUnit argument) {
assertNotNull(argument.name());
}
String实例隐式转换为以下目标类型。
十进制、十六进制和八进制String文字将转换为其整数类型:byte、short、int、long及其盒装对应项。
|
| 目标类型 | 例子 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
回退字符串到对象的转换
除了从字符串到上表中列出的目标类型的隐式转换之外,JUnit Jupiter 还提供了一种后备机制,
String如果目标类型恰好声明了一个合适的工厂方法或定义的工厂构造函数,则可以自动从 转换为给定的目标类型以下。
-
工厂方法:在目标类型中声明的非私有
static方法,它接受单个String参数并返回目标类型的实例。方法的名称可以是任意的,不需要遵循任何特定的约定。 -
工厂构造函数:目标类型中接受单个参数的非私有构造函数
String。请注意,目标类型必须声明为顶级类或static嵌套类。
| 如果发现多个工厂方法,它们将被忽略。如果发现 工厂方法和工厂构造函数,则将使用工厂方法而不是构造函数。 |
例如,在以下@ParameterizedTest方法中,将通过调用工厂方法并
作为书名传递Book来创建参数。Book.fromTitle(String)"42 Cats"
@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
assertEquals("42 Cats", book.getTitle());
}
public class Book {
private final String title;
private Book(String title) {
this.title = title;
}
public static Book fromTitle(String title) {
return new Book(title);
}
public String getTitle() {
return this.title;
}
}
显式转换
您可以使用注释显式指定
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.16.5。参数聚合
默认情况下,提供给方法的每个参数@ParameterizedTest都对应于一个方法参数。因此,预期提供大量参数的参数源可能会导致较大的方法签名。
ArgumentsAccessor在这种情况下,可以使用an来代替多个参数。使用此 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。
定制聚合器
除了@ParameterizedTest使用 直接访问方法的参数
之外ArgumentsAccessor,JUnit Jupiter 还支持使用自定义的、可重用的
聚合器。
要使用自定义聚合器,请实现接口并通过方法中兼容参数上的注释
ArgumentsAggregator进行注册。当调用参数化测试时,聚合的结果将作为相应参数的参数提供。请注意, 的实现必须声明为顶级类或嵌套类。@AggregateWith@ParameterizedTestArgumentsAggregatorstatic
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
// perform assertions against person
}
public class PersonAggregator implements ArgumentsAggregator {
@Override
public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) {
return new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, Gender.class),
arguments.get(3, LocalDate.class));
}
}
如果您发现自己@AggregateWith(MyTypeAggregator.class)在代码库中重复声明多个参数化测试方法,您可能希望创建一个自定义
组合注释,例如@CsvToMyType使用
@AggregateWith(MyTypeAggregator.class). 以下示例通过自定义注释演示了这一点@CsvToPerson。
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
// perform assertions against person
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}
2.16.6。自定义显示名称
默认情况下,参数化测试调用的显示名称包含调用索引以及String该特定调用的所有参数的表示。如果存在于字节码中(对于 Java,测试代码必须使用编译器标志进行编译),每个参数前面都有参数名称(除非参数只能通过 或 获得ArgumentsAccessor)。ArgumentAggregator-parameters
但是,您可以通过注释name的属性
自定义调用显示名称@ParameterizedTest,如下例所示。
@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
void testWithCustomDisplayNames(String fruit, int rank) {
}
使用执行上述方法时,ConsoleLauncher您将看到类似于以下内容的输出。
Display name of container ✔ ├─ 1 ==> the rank of 'apple' is 1 ✔ ├─ 2 ==> the rank of 'banana' is 2 ✔ └─ 3 ==> the rank of 'lemon, lime' is 3 ✔
请注意,这name是一个MessageFormat模式。因此,单引号 ( ') 需要表示为双单引号 ( '') 才能显示。
自定义显示名称支持以下占位符。
| 占位符 | 描述 |
|---|---|
|
方法的显示名称 |
|
当前调用索引(从 1 开始) |
|
完整的、以逗号分隔的参数列表 |
|
包含参数名称的完整的、以逗号分隔的参数列表 |
|
个人论点 |
当显示名称中包含参数时,如果它们超过配置的最大长度,它们的字符串表示形式将被截断。该限制可通过
junit.jupiter.params.displayname.argument.maxlength配置参数进行配置,默认为 512 个字符。
|
使用@MethodSource或时@ArgumentsSource,您可以使用 API 为参数提供自定义名称Named。如果调用显示名称中包含参数,则将使用自定义名称,如下例所示。
@DisplayName("A parameterized test with named arguments")
@ParameterizedTest(name = "{index}: {0}")
@MethodSource("namedArguments")
void testWithNamedArguments(File file) {
}
static Stream<Arguments> namedArguments() {
return Stream.of(
arguments(named("An important file", new File("path1"))),
arguments(named("Another file", new File("path2")))
);
}
A parameterized test with named arguments ✔ ├─ 1: An important file ✔ └─ 2: Another file ✔
如果您想为项目中的所有参数化测试设置默认名称模式,您可以junit.jupiter.params.displayname.default在文件中声明配置参数junit-platform.properties,如以下示例所示(有关其他选项,请参阅
配置参数)。
junit.jupiter.params.displayname.default = {index}
参数化测试的显示名称根据以下优先规则确定:
-
name中的属性@ParameterizedTest(如果存在) -
配置参数的值
junit.jupiter.params.displayname.default(如果存在) -
DEFAULT_DISPLAY_NAME常数定义在@ParameterizedTest
2.16.7。生命周期和互操作性
参数化测试的每次调用都具有与常规@Test
方法相同的生命周期。例如,@BeforeEach方法将在每次调用之前执行。与动态测试类似,调用将一一出现在 IDE 的测试树中。您可以随意
在同一个测试类中混合常规@Test方法和方法。@ParameterizedTest
您可以将ParameterResolver扩展与@ParameterizedTest方法一起使用。但是,由参数源解析的方法参数需要位于参数列表的第一位。由于测试类可能包含常规测试以及具有不同参数列表的参数化测试,因此不会为生命周期方法(例如@BeforeEach)和测试类构造函数解析来自参数源的值。
@BeforeEach
void beforeEach(TestInfo testInfo) {
// ...
}
@ParameterizedTest
@ValueSource(strings = "apple")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
testReporter.publishEntry("argument", argument);
}
@AfterEach
void afterEach(TestInfo testInfo) {
// ...
}
2.17。测试模板
方法@TestTemplate不是常规测试用例,而是测试用例的模板。因此,它被设计为根据注册提供者返回的调用上下文的数量被多次调用。因此,它必须与注册的扩展一起使用TestTemplateInvocationContextProvider。测试模板方法的每次调用的行为类似于常规方法的执行@Test
,完全支持相同的生命周期回调和扩展。请参阅
为测试模板提供调用上下文以获取使用示例。
2.18。动态测试
注解@Test中描述的 JUnit Jupiter 中的
标准注解与 JUnit 4 中的注解非常相似。两者都描述了实现测试用例的方法。这些测试用例是静态的,因为它们在编译时完全指定,并且它们的行为不能被运行时发生的任何事情改变。假设提供了动态行为的基本形式,但有意限制其表达能力。@Test
除了这些标准测试之外,JUnit Jupiter 中还引入了一种全新的测试编程模型。这种新型测试是动态测试,由带有 注释的工厂方法在运行时生成@TestFactory。
@Test与方法相反,@TestFactory方法本身不是测试用例,而是测试用例的工厂。因此,动态测试是工厂的产品。从技术上讲,方法必须返回@TestFactory单个实例或实例DynamicNode数组
。的可实例化子类是和。
实例由显示名称和动态子节点列表组成,允许创建任意嵌套的动态节点层次结构。
实例将被延迟执行,从而能够动态甚至不确定地生成测试用例。StreamCollectionIterableIteratorDynamicNodeDynamicNodeDynamicContainerDynamicTestDynamicContainerDynamicTest
Streama 返回的任何内容都@TestFactory将通过调用正确关闭
stream.close(),从而可以安全地使用诸如 之类的资源Files.lines()。
与@Test方法一样,@TestFactory方法不能是private或static可以选择声明要由 解析的参数ParameterResolvers。
ADynamicTest是运行时生成的测试用例。它由显示名称
和Executable. Executable是 ,@FunctionalInterface这意味着动态测试的实现可以作为lambda 表达式或方法引用提供。
|
动态测试生命周期
动态测试的执行生命周期与标准用例有很大不同@Test。具体来说,单个动态测试没有生命周期回调。这意味着方法@BeforeEach及其@AfterEach相应的扩展回调是针对该@TestFactory方法执行的,而不是针对每个动态测试执行的。换句话说,如果您在动态测试的 lambda 表达式中访问测试实例中的字段,则在执行由同一方法生成的各个动态测试之间的回调方法或扩展不会重置这些字段
@TestFactory。
|
从 JUnit Jupiter 5.10.1 开始,动态测试必须始终通过工厂方法创建;然而,这可能会在以后的版本中通过注册工具来补充。
2.18.1. 动态测试示例
下面的DynamicTestsDemo课程演示了测试工厂和动态测试的几个示例。
第一个方法返回无效的返回类型。由于在编译时无法检测到无效的返回类型,因此JUnitException在运行时检测到时会抛出 a 。
Collection接下来的六个方法演示了、Iterable、Iterator、 数组或Stream实例的生成DynamicTest。这些示例中的大多数并没有真正展示动态行为,而只是原则上演示支持的返回类型。但是,dynamicTestsFromStream()并dynamicTestsFromIntStream()演示如何为给定的一组字符串或一系列输入数字生成动态测试。
下一个方法本质上是真正动态的。generateRandomNumberOfTests()实现
Iterator生成随机数、显示名称生成器和测试执行器的 ,然后将所有三个提供给DynamicTest.stream(). 尽管 的非确定性行为generateRandomNumberOfTests()当然与测试可重复性相冲突,因此应谨慎使用,但它可以证明动态测试的表现力和力量。
generateRandomNumberOfTests()下一个方法在灵活性方面与下一个类似;然而,通过工厂方法dynamicTestsFromStreamFactoryMethod()从现有的动态测试流生成。StreamDynamicTest.stream()
出于演示目的,该dynamicNodeSingleTest()方法生成单个
DynamicTest流而不是流,并且该dynamicNodeSingleContainer()方法利用 生成动态测试的嵌套层次结构DynamicContainer。
import static example.util.StringUtils.isPalindrome;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import static org.junit.jupiter.api.Named.named;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import example.util.Calculator;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
class DynamicTestsDemo {
private final Calculator calculator = new Calculator();
// This will result in a JUnitException!
@TestFactory
List<String> dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
}
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(
dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
).iterator();
}
@TestFactory
DynamicTest[] dynamicTestsFromArray() {
return new DynamicTest[] {
dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
};
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
// Generates tests for the first 10 even integers.
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {
// Generates random positive integers between 0 and 100 until
// a number evenly divisible by 7 is encountered.
Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random();
int current;
@Override
public boolean hasNext() {
current = random.nextInt(100);
return current % 7 != 0;
}
@Override
public Integer next() {
return current;
}
};
// Generates display names like: input:5, input:37, input:85, etc.
Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
// Executes tests based on the current input value.
ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
// Stream of palindromes to check
Stream<String> inputStream = Stream.of("racecar", "radar", "mom", "dad");
// Generates display names like: racecar is a palindrome
Function<String, String> displayNameGenerator = text -> text + " is a palindrome";
// Executes tests based on the current input value.
ThrowingConsumer<String> testExecutor = text -> assertTrue(isPalindrome(text));
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethodWithNames() {
// Stream of palindromes to check
Stream<Named<String>> inputStream = Stream.of(
named("racecar is a palindrome", "racecar"),
named("radar is also a palindrome", "radar"),
named("mom also seems to be a palindrome", "mom"),
named("dad is yet another palindrome", "dad")
);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream,
text -> assertTrue(isPalindrome(text)));
}
@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of("A", "B", "C")
.map(input -> dynamicContainer("Container " + input, Stream.of(
dynamicTest("not null", () -> assertNotNull(input)),
dynamicContainer("properties", Stream.of(
dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
))
)));
}
@TestFactory
DynamicNode dynamicNodeSingleTest() {
return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));
}
@TestFactory
DynamicNode dynamicNodeSingleContainer() {
return dynamicContainer("palindromes",
Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
));
}
}
2.18.2. 动态测试的 URI 测试源
JUnit 平台提供了TestSource测试或容器源的表示,用于通过 IDE 和构建工具导航到其位置。
TestSource动态测试或动态容器的 可以
分别通过或工厂方法java.net.URI提供。将转换为以下实现之一
。DynamicTest.dynamicTest(String, URI,
Executable)DynamicContainer.dynamicContainer(String, URI, Stream)URITestSource
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[]). 请参阅 Javadoc 了解DiscoverySelectors.selectMethod(String)FQMN 支持的格式。 ClassSource-
如果
URI包含class方案和完全限定的类名 - 例如,class:org.junit.Foo?line=42. UriSource-
如果上述
TestSource实现均不适用。
2.19。超时
该@Timeout注释允许声明如果测试、测试工厂、测试模板或生命周期方法的执行时间超过给定的持续时间,则该测试、测试工厂、测试模板或生命周期方法应该失败。持续时间的时间单位默认为秒,但可以配置。
以下示例展示了如何@Timeout应用于生命周期和测试方法。
class TimeoutDemo {
@BeforeEach
@Timeout(5)
void setUp() {
// fails if execution time exceeds 5 seconds
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds500Milliseconds() {
// fails if execution time exceeds 500 milliseconds
}
@Test
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS, threadMode = ThreadMode.SEPARATE_THREAD)
void failsIfExecutionTimeExceeds500MillisecondsInSeparateThread() {
// fails if execution time exceeds 500 milliseconds, the test code is executed in a separate thread
}
}
要将相同的超时应用于测试类及其所有@Nested
类中的所有测试方法,您可以@Timeout在类级别声明注释。然后,它将应用于该类及其类中的所有测试、测试工厂和测试模板方法
,除非被特定方法或类上的注释
@Nested覆盖。请注意,在类级别声明的注释不适用于生命周期方法。@Timeout@Nested@Timeout
@Timeout在方法上声明@TestFactory会检查工厂方法是否在指定的持续时间内返回,但不会验证
DynamicTest工厂生成的每个单独的执行时间。请
为此目的使用assertTimeout()或。assertTimeoutPreemptively()
如果方法@Timeout上存在@TestTemplate- 例如,@RepeatedTest或
@ParameterizedTest- 每个调用都会应用给定的超时。
2.19.1。线程模式
可以使用以下三种线程模式之一应用超时:SAME_THREAD、
SEPARATE_THREAD或INFERRED。
当SAME_THREAD使用 时,带注释的方法的执行将在测试的主线程中进行。如果超过超时,主线程将被另一个线程中断。这样做是为了确保与 Spring 等框架的互操作性,这些框架利用对当前运行的线程敏感的机制(例如ThreadLocal事务管理)。
相反,当SEPARATE_THREAD使用 时,就像assertTimeoutPreemptively()
断言一样,带注释的方法的执行在单独的线程中进行,这可能会导致不良的副作用,请参阅Preemptive Timeouts withassertTimeoutPreemptively()。
当INFERRED使用(默认)线程模式时,线程模式通过配置参数解析
junit.jupiter.execution.timeout.thread.mode.default。如果提供的配置参数无效或不存在,则SAME_THREAD用作后备。
2.19.2. 默认超时
以下配置参数可用于指定特定类别的所有方法的默认超时,除非它们或封闭的测试类用 进行注释@Timeout:
junit.jupiter.execution.timeout.default-
所有可测试和生命周期方法的默认超时
junit.jupiter.execution.timeout.testable.method.default-
所有可测试方法的默认超时
junit.jupiter.execution.timeout.test.method.default-
@Test方法的默认超时 junit.jupiter.execution.timeout.testtemplate.method.default-
@TestTemplate方法的默认超时 junit.jupiter.execution.timeout.testfactory.method.default-
@TestFactory方法的默认超时 junit.jupiter.execution.timeout.lifecycle.method.default-
所有生命周期方法的默认超时
junit.jupiter.execution.timeout.beforeall.method.default-
@BeforeAll方法的默认超时 junit.jupiter.execution.timeout.beforeeach.method.default-
@BeforeEach方法的默认超时 junit.jupiter.execution.timeout.aftereach.method.default-
@AfterEach方法的默认超时 junit.jupiter.execution.timeout.afterall.method.default-
@AfterAll方法的默认超时
更具体的配置参数会覆盖不太具体的配置参数。例如,
junit.jupiter.execution.timeout.test.method.default覆盖
junit.jupiter.execution.timeout.testable.method.default覆盖
junit.jupiter.execution.timeout.default.
此类配置参数的值必须采用以下不区分大小写的格式:<number> [ns|μs|ms|s|m|h|d]。数字和单位之间的空格可以省略。不指定单位相当于使用秒。
| 参数值 | 等效注释 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2.19.3。使用@Timeout进行轮询测试
在处理异步代码时,通常会编写轮询测试,同时等待某些事情发生,然后再执行任何断言。在某些情况下,您可以重写逻辑以使用一种CountDownLatch或另一种同步机制,但有时这是不可能的 - 例如,如果被测试的主题将消息发送到外部消息代理中的通道,并且在该消息之前无法执行断言已通过通道成功发送。像这样的异步测试需要某种形式的超时,以确保它们不会无限期地执行而挂起测试套件,就像异步消息从未成功传递的情况一样。
通过为轮询的异步测试配置超时,您可以确保测试不会无限期地执行。以下示例演示了如何使用 JUnit Jupiter 的@Timeout注释来实现此目的。该技术可用于非常轻松地实现“轮询直到”逻辑。
@Test
@Timeout(5) // Poll at most 5 seconds
void pollUntil() throws InterruptedException {
while (asynchronousResultNotAvailable()) {
Thread.sleep(250); // custom poll interval
}
// Obtain the asynchronous result and perform assertions
}
| 如果您需要对轮询间隔进行更多控制并通过异步测试获得更大灵活性,请考虑使用专用库,例如 Awaitility。 |
2.20. 并行执行
默认情况下,JUnit Jupiter 测试在单个线程中按顺序运行。从 5.3 版开始,并行运行测试(例如,为了加快执行速度)作为一项可选功能提供。要启用并行执行,请将
junit.jupiter.execution.parallel.enabled配置参数设置为true — 例如 in 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或 a 的
测试类MethodOrderer(除了MethodOrderer.Random)。在前一种情况下,测试作者必须确保测试类是线程安全的;在后者中,并发执行可能与配置的执行顺序冲突。@Execution(CONCURRENT)
因此,在这两种情况下,只有在测试类或方法上存在注释时,此类测试类中的测试方法才会并发执行。
当启用并行执行并ClassOrderer注册默认值时(有关详细信息,请参阅
类顺序),顶级测试类最初将相应地排序并按该顺序安排。但是,不能保证它们完全按照该顺序启动,因为执行它们的线程不是由 JUnit 直接控制的。
此外,您还可以通过设置配置参数来配置顶级类的默认执行模式junit.jupiter.execution.parallel.mode.classes.default。通过组合这两个配置参数,您可以将类配置为并行运行,但它们的方法在同一线程中:
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = same_thread
junit.jupiter.execution.parallel.mode.classes.default = concurrent
相反的组合将并行运行一个类中的所有方法,但顶级类将按顺序运行:
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = same_thread
下图说明了两个顶级测试类A以及
B每个类有两个测试方法的执行对于
junit.jupiter.execution.parallel.mode.default和
的所有四种组合的行为junit.jupiter.execution.parallel.mode.classes.default(请参阅第一列中的标签)。
如果未显式设置配置参数,则将使用 的junit.jupiter.execution.parallel.mode.classes.default值。junit.jupiter.execution.parallel.mode.default
2.20.1。配置
可以使用 来配置所需的并行性和最大池大小等属性ParallelExecutionConfigurationStrategy。JUnit 平台提供了两种开箱即用的实现: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-
ParallelExecutionConfigurationStrategy允许您通过强制配置参数指定自定义实现junit.jupiter.execution.parallel.config.custom.class,以确定所需的配置。
如果未设置配置策略,JUnit Jupiter 将使用dynamic因子为 的配置策略1。因此,所需的并行性将等于可用处理器/内核的数量。
|
并行性本身并不意味着最大并发线程数
默认情况下,JUnit Jupiter 不保证并发执行测试的数量不会超过配置的并行度。例如,当使用下一节中描述的同步机制之一时,在ForkJoinPool幕后使用的同步机制可能会产生额外的线程,以确保执行以足够的并行性继续进行。如果您需要这样的保证,在 Java 9+ 中,可以通过控制dynamic、fixed和
custom策略的最大池大小来限制最大并发线程数。
|
相关属性
下表列出了配置并行执行的相关属性。有关如何设置此类属性的详细信息,请参阅 配置参数。
| 财产 | 描述 | 支持的值 | 默认值 |
|---|---|---|---|
|
启用并行测试执行 |
|
|
|
测试树中节点的默认执行模式 |
|
|
|
顶级类的默认执行模式 |
|
|
|
所需并行性和最大池大小的执行策略 |
|
|
|
乘以可用处理器/内核数量的系数,以确定 |
正十进制数 |
|
|
乘以可用处理器/内核数量和 值的因子,
|
正小数,必须大于或等于 |
|
|
|
|
|
|
|
正整数 |
无默认值 |
|
|
正整数,必须大于或等于 |
256 + 的值 |
|
|
|
|
|
用于配置策略的ParallelExecutionConfigurationStrategy的完全限定类名 |
例如,org.example.CustomStrategy |
无默认值 |
2.20.2. 同步
除了使用@Execution注解来控制执行模式之外,JUnit Jupiter 还提供了另一种基于注解的声明式同步机制。该
@ResourceLock注释允许您声明测试类或方法使用需要同步访问的特定共享资源以确保可靠的测试执行。共享资源由唯一的名称标识,该名称是String. 该名称可以是用户定义的或以下中的预定义常量之一Resources:
SYSTEM_PROPERTIES、SYSTEM_OUT、SYSTEM_ERR、LOCALE或TIME_ZONE。
如果以下示例中的测试在不使用 @ResourceLock的情况下并行运行,那么它们将是不稳定的。有时它们会通过,而有时它们会由于写入然后读取相同 JVM 系统属性的固有竞争条件而失败。
当使用注释声明对共享资源的访问时@ResourceLock,JUnit Jupiter 引擎使用此信息来确保并行运行没有冲突的测试。
|
隔离运行测试
如果您的大多数测试类可以并行运行而无需任何同步,但您有一些测试类需要隔离运行,则可以使用注释标记后者
|
除了String唯一标识共享资源之外,您还可以指定访问模式。需要READ访问共享资源的两个测试可以彼此并行运行,但在需要READ_WRITE访问同一共享资源的任何其他测试正在运行时则不能并行运行。
@Execution(CONCURRENT)
class SharedResourcesDemo {
private Properties backup;
@BeforeEach
void backup() {
backup = new Properties();
backup.putAll(System.getProperties());
}
@AfterEach
void restore() {
System.setProperties(backup);
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ)
void customPropertyIsNotSetByDefault() {
assertNull(System.getProperty("my.prop"));
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
void canSetCustomPropertyToApple() {
System.setProperty("my.prop", "apple");
assertEquals("apple", System.getProperty("my.prop"));
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
void canSetCustomPropertyToBanana() {
System.setProperty("my.prop", "banana");
assertEquals("banana", System.getProperty("my.prop"));
}
}
2.21。内置扩展
虽然 JUnit 团队鼓励将可重用扩展打包并维护在单独的库中,但 JUnit Jupiter API 工件包含一些面向用户的扩展实现,这些扩展实现被认为非常有用,以至于用户不必添加其他依赖项。
2.21.1。临时目录扩展
内置TempDirectory扩展用于为单个测试或测试类中的所有测试创建和清理临时目录。默认情况下已注册。要使用它,请注释一个非最终的、未分配的 type java.nio.file.Pathor
java.io.Filewith字段,或者将一个 type or
注释@TempDir的参数添加到生命周期方法或测试方法中。java.nio.file.Pathjava.io.File@TempDir
例如,以下测试@TempDir为单个测试方法声明一个注释为 的参数,在临时目录中创建并写入一个文件,并检查其内容。
@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
您可以通过指定多个带注释的参数来注入多个临时目录。
@Test
void copyFileFromSourceToTarget(@TempDir Path source, @TempDir Path target) throws IOException {
Path sourceFile = source.resolve("test.txt");
new ListWriter(sourceFile).write("a", "b", "c");
Path targetFile = Files.copy(sourceFile, target.resolve("test.txt"));
assertNotEquals(sourceFile, targetFile);
assertEquals(singletonList("a,b,c"), Files.readAllLines(targetFile));
}
要恢复为整个测试类或方法使用单个临时目录的旧行为(取决于使用注释的级别),您可以将配置参数设置junit.jupiter.tempdir.scope为per_context。但是,请注意,此选项已弃用,并将在未来版本中删除。
|
@TempDir构造函数参数不支持。如果您希望跨生命周期方法和当前测试方法保留对临时目录的单个引用,请通过使用@TempDir.
以下示例将共享临时目录存储在字段中static。sharedTempDir这允许在测试类的所有生命周期方法和测试方法中使用相同的方法。为了更好地隔离,您应该使用实例字段,以便每个测试方法使用单独的目录。
class SharedTempDirectoryDemo {
@TempDir
static Path sharedTempDir;
@Test
void writeItemsToFile() throws IOException {
Path file = sharedTempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
@Test
void anotherTestThatUsesTheSameTempDir() {
// use sharedTempDir
}
}
该@TempDir注释有一个可选cleanup属性,可以设置为
NEVER、ON_SUCCESS或ALWAYS。如果清理模式设置为NEVER,则测试完成后不会删除临时目录。如果设置为ON_SUCCESS,则仅在测试成功完成后才会删除临时目录。
默认的清理模式是ALWAYS. 您可以使用
junit.jupiter.tempdir.cleanup.mode.default
配置参数来覆盖此默认值。
class CleanupModeDemo {
@Test
void fileTest(@TempDir(cleanup = ON_SUCCESS) Path tempDir) {
// perform test
}
}
@TempDir支持通过可选属性以编程方式创建临时目录
factory。这通常用于控制临时目录的创建,例如定义父目录或应使用的文件系统。
工厂可以通过实施来创建TempDirFactory。实现必须提供无参数构造函数,并且不应对它们实例化的时间和次数做出任何假设,但它们可以假设它们的 和 方法createTempDirectory(…)将
close()按此顺序在每个实例中从同一线程调用一次。
Jupiter 中可用的默认实现将目录创建委托给
java.nio.file.Files::createTempDirectory,junit并作为用于生成目录名称的前缀字符串传递。
以下示例定义了一个使用测试名称作为目录名称前缀而不是junit常量值的工厂。
class TempDirFactoryDemo {
@Test
void factoryTest(@TempDir(factory = Factory.class) Path tempDir) {
assertTrue(tempDir.getFileName().toString().startsWith("factoryTest"));
}
static class Factory implements TempDirFactory {
@Override
public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
throws IOException {
return Files.createTempDirectory(extensionContext.getRequiredTestMethod().getName());
}
}
}
还可以使用内存中的文件系统,例如Jimfs创建临时目录。以下示例演示了如何实现这一目标。
class InMemoryTempDirDemo {
@Test
void test(@TempDir(factory = JimfsTempDirFactory.class) Path tempDir) {
// perform test
}
static class JimfsTempDirFactory implements TempDirFactory {
private final FileSystem fileSystem = Jimfs.newFileSystem(Configuration.unix());
@Override
public Path createTempDirectory(AnnotatedElementContext elementContext, ExtensionContext extensionContext)
throws IOException {
return Files.createTempDirectory(fileSystem.getPath("/"), "junit");
}
@Override
public void close() throws IOException {
fileSystem.close();
}
}
}
@TempDir也可以用作元注释以减少重复。以下代码清单显示了如何创建@JimfsTempDir
可用作
@TempDir(factory = JimfsTempDirFactory.class).
@TempDir@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@TempDir(factory = JimfsTempDirFactory.class)
@interface JimfsTempDir {
}
以下示例演示了如何使用自定义@JimfsTempDir注释。
class JimfsTempDirAnnotationDemo {
@Test
void test(@JimfsTempDir Path tempDir) {
// perform test
}
}
声明注释的字段或参数上的元注释或附加注释TempDir
可能会公开附加属性来配置工厂。AnnotatedElementContext
此类注释和相关属性可以通过的参数访问createTempDirectory。
您可以使用junit.jupiter.tempdir.factory.default
配置参数TempDirFactory来指定默认情况下要使用的完全限定类名。就像通过注释factory的属性配置的工厂一样@TempDir,提供的类必须实现该TempDirFactory接口。默认工厂将用于所有@TempDir注释,除非factory注释的属性指定不同的工厂。
总之,临时目录的工厂是根据以下优先规则确定的:
-
factory注释的属性(@TempDir如果存在) -
TempDirFactory通过配置参数配置的默认值(如果存在) -
否则,
org.junit.jupiter.api.io.TempDirFactory$Standard将被使用。
3. 从 JUnit 4 迁移
尽管 JUnit Jupiter 编程模型和扩展模型本身不支持 JUnit 4 功能,例如Rules和Runners,但预计源代码维护者不需要更新所有现有测试、测试扩展和自定义构建测试基础架构以迁移到 JUnit木星。
相反,JUnit 通过JUnit Vintage 测试引擎提供了一条温和的迁移路径,允许使用 JUnit 平台基础设施执行基于 JUnit 3 和 JUnit 4 的现有测试。由于特定于 JUnit Jupiter 的所有类和注释都位于org.junit.jupiter基础包下,因此在类路径中同时包含 JUnit 4 和 JUnit Jupiter 不会导致任何冲突。因此,与 JUnit Jupiter 测试一起维护现有的 JUnit 4 测试是安全的。此外,由于 JUnit 团队将继续为 JUnit 4.x 基线提供维护和错误修复版本,因此开发人员有充足的时间按照自己的计划迁移到 JUnit Jupiter。
3.1. 在 JUnit 平台上运行 JUnit 4 测试
确保该junit-vintage-engine工件位于您的测试运行时路径中。在这种情况下,JUnit 平台启动器将自动选取 JUnit 3 和 JUnit 4 测试。
请参阅存储库中的示例项目,junit5-samples了解如何使用 Gradle 和 Maven 完成此操作。
3.2. 迁移技巧
以下是将现有 JUnit 4 测试迁移到 JUnit Jupiter 时应注意的主题。
-
注释位于
org.junit.jupiter.api包中。 -
断言驻留在
org.junit.jupiter.api.Assertions. -
假设位于
org.junit.jupiter.api.Assumptions.-
请注意,JUnit Jupiter 5.4 及更高版本支持 JUnit 4
org.junit.Assume类中的方法进行假设。具体来说,JUnit Jupiter 支持 JUnit 4AssumptionViolatedException来发出信号,表明测试应该中止,而不是标记为失败。
-
-
@Before并且@After不再存在;使用@BeforeEachand@AfterEach代替。 -
@BeforeClass并且@AfterClass不再存在;使用@BeforeAlland@AfterAll代替。 -
@Ignore不再存在:使用@Disabled或其他内置 执行条件之一代替-
另请参阅JUnit 4 @Ignore 支持。
-
-
@Category不复存在;@Tag代替使用。 -
@RunWith不复存在; 被 取代@ExtendWith。 -
@Rule并且@ClassRule不再存在;@ExtendWith被和 取代@RegisterExtension。-
另请参阅有限的 JUnit 4 规则支持。
-
-
@Test(expected = …)并且ExpectedException规则不再存在;Assertions.assertThrows(…)代替使用 。-
如果您仍然需要使用, 请参阅有限的 JUnit 4 规则支持
ExpectedException。
-
-
JUnit Jupiter 中的断言和假设接受失败消息作为它们的最后一个参数而不是第一个参数。
-
有关详细信息,请参阅失败消息参数。
-
3.3. 有限的 JUnit 4 规则支持
如上所述,JUnit Jupiter 本身不支持也不会支持 JUnit 4 规则。然而,JUnit 团队意识到,许多组织,尤其是大型组织,可能拥有使用自定义规则的大型 JUnit 4 代码库。为了服务这些组织并实现逐步迁移路径,JUnit 团队决定在 JUnit Jupiter 中逐字支持选择 JUnit 4 规则。这种支持基于适配器,并且仅限于那些在语义上与 JUnit Jupiter 扩展模型兼容的规则,即那些不会完全改变测试的整体执行流程的规则。
JUnit Jupiter 的模块junit-jupiter-migrationsupport当前支持以下三种Rule类型,包括这些类型的子类:
-
org.junit.rules.ExternalResource(包括org.junit.rules.TemporaryFolder) -
org.junit.rules.Verifier(包括org.junit.rules.ErrorCollector) -
org.junit.rules.ExpectedException
与 JUnit 4 一样,支持规则注释字段和方法。通过在测试类上使用这些类级扩展,Rule遗留代码库中的此类实现可以保持不变,包括 JUnit 4 规则导入语句。
这种有限形式的Rule支持可以通过类级注释来打开
@EnableRuleMigrationSupport。此注释是一个组合注释,可启用所有规则迁移支持扩展:VerifierSupport、ExternalResourceSupport和
ExpectedExceptionSupport。您也可以选择注释您的测试类,用于
@EnableJUnit4MigrationSupport注册规则和JUnit 4@Ignore注释的迁移支持(请参阅JUnit 4 @Ignore Support)。
但是,如果您打算为 JUnit Jupiter 开发新的扩展,请使用 JUnit Jupiter 的新扩展模型,而不是 JUnit 4 基于规则的模型。
3.4. JUnit 4 @忽略支持
为了提供从 JUnit 4 到 JUnit Jupiter 的平滑迁移路径,该
junit-jupiter-migrationsupport模块提供了对 JUnit 4@Ignore
注释的支持,类似于 Jupiter 的@Disabled注释。
@Ignore要与基于 JUnit Jupiter 的测试一起使用,请在构建中配置对
模块的测试junit-jupiter-migrationsupport依赖项,然后使用@ExtendWith(IgnoreCondition.class)或注释您的测试类@EnableJUnit4MigrationSupport(这会自动注册IgnoreCondition以及
Limited JUnit 4 Rule Support)。是IgnoreCondition禁用
ExecutionCondition用 注释的测试类或测试方法的
@Ignore。
import org.junit.Ignore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.migrationsupport.EnableJUnit4MigrationSupport;
// @ExtendWith(IgnoreCondition.class)
@EnableJUnit4MigrationSupport
class IgnoredTestsDemo {
@Ignore
@Test
void testWillBeIgnored() {
}
@Test
void testWillBeExecuted() {
}
}
3.5. 失败消息参数
JUnit Jupiter 中的和Assumptions类Assertions以与 JUnit 4 中不同的顺序声明参数。在 JUnit 4 中,断言和假设方法接受失败消息作为第一个参数;而在 JUnit Jupiter 断言和假设方法中,接受失败消息作为最后一个参数。
例如,JUnit 4 中的方法assertEquals被声明为assertEquals(String
message, Object expected, Object actual),但在 JUnit Jupiter 中它被声明为
assertEquals(Object expected, Object actual, String message)。这样做的理由是失败消息是可选的,并且可选参数应该在方法签名中的必需参数之后声明。
受此更改影响的方法如下:
-
断言
-
assertTrue -
assertFalse -
assertNull -
assertNotNull -
assertEquals -
assertNotEquals -
assertArrayEquals -
assertSame -
assertNotSame -
assertThrows
-
-
假设
-
assumeTrue -
assumeFalse
-
4. 运行测试
4.1. IDE支持
4.1.1. 智能IDEA
自 2016.2 版本起,IntelliJ IDEA 支持在 JUnit 平台上运行测试。有关详细信息,请参阅
IntelliJ IDEA 博客上的帖子。但请注意,建议使用 IDEA 2017.3 或更高版本,因为这些较新版本的 IDEA 会根据项目中使用的 API 版本自动下载以下 JAR:junit-platform-launcher、
junit-jupiter-engine和junit-vintage-engine。
| IntelliJ IDEA 在 IDEA 2017.3 之前发布,捆绑特定版本的 JUnit 5。因此,如果您想使用较新版本的 JUnit Jupiter,IDE 中的测试执行可能会因版本冲突而失败。在这种情况下,请按照以下说明使用比 IntelliJ IDEA 捆绑版本更新的 JUnit 5 版本。 |
为了使用不同的 JUnit 5 版本(例如 5.10.1),您可能需要在类路径中包含相应版本的junit-platform-launcher、
junit-jupiter-engine和JAR。junit-vintage-engine
testImplementation(platform("org.junit:junit-bom:5.10.1"))
testRuntimeOnly("org.junit.platform:junit-platform-launcher") {
because("Only needed to run tests in a version of IntelliJ IDEA that bundles older versions")
}
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
<!-- ... -->
<dependencies>
<!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.10.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
4.1.2. 蚀
自 Eclipse Oxygen.1a (4.7.1a) 发布以来,Eclipse IDE 提供了对 JUnit 平台的支持。
有关在 Eclipse 中使用 JUnit 5 的更多信息,请参阅Eclipse 项目 Oxygen.1a (4.7.1a) - 新的和值得注意的文档的官方Eclipse 对 JUnit 5 的支持部分 。
4.1.3. 网豆
自Apache NetBeans 10.0 发布以来,NetBeans 就提供了对 JUnit Jupiter 和 JUnit Platform 的支持 。
有关详细信息,请参阅Apache NetBeans 10.0 发行说明的 JUnit 5 部分 。
4.1.4. 视觉工作室代码
Visual Studio Code通过Java Test Runner扩展支持 JUnit Jupiter 和 JUnit Platform, 该扩展默认作为 Java Extension Pack 的一部分安装。
有关详细信息,请参阅Visual Studio Code 文档中的 Java测试部分 。
4.1.5. 其他 IDE
如果您使用的编辑器或 IDE 不是前面几节中列出的编辑器或 IDE,JUnit 团队提供了两种替代解决方案来帮助您使用 JUnit 5。您可以手动使用控制台启动器(例如,从命令行)或者如果您的 IDE 内置了对 JUnit 4 的支持,则使用基于 JUnit 4 的 Runner执行测试。
4.2. 建立支持
4.2.1. 摇篮
test {
useJUnitPlatform()
}
test {
useJUnitPlatform {
includeTags("fast", "smoke & feature-a")
// excludeTags("slow", "ci")
includeEngines("junit-jupiter")
// excludeEngines("junit-vintage")
}
}
请参阅 官方 Gradle 文档 以获取完整的选项列表。
调整依赖版本
除非您使用定义了自己的依赖关系管理方式的 Spring Boot,否则建议使用 JUnit 平台 BOM 来对齐所有 JUnit 5 工件的版本。
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.1"))
}
使用 BOM,您可以在使用org.junit.platform、org.junit.jupiter和org.junit.vintage组 ID 声明所有工件的依赖项时省略版本。
| 有关如何覆盖 Spring Boot 应用程序中使用的 JUnit 版本的详细信息,请 参阅Spring Boot 。 |
配置参数
标准 Gradletest任务当前不提供专用 DSL 来设置 JUnit 平台配置参数以影响测试发现和执行。但是,您可以通过系统属性(如下所示)或通过文件在构建脚本中提供配置参数
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 的测试的支持,请配置testImplementation对依赖项聚合 JUnit Jupiter 工件的依赖项,如下所示。
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1") // version can be omitted when using the BOM
}
只要您配置testImplementation
对 JUnit 4 的依赖项和testRuntimeOnly对 JUnit VintageTestEngine
实现的依赖项(类似于以下内容),JUnit 平台就可以运行基于 JUnit 4 的测试。
dependencies {
testImplementation("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.10.1") // version can be omitted when using the BOM
}
配置日志记录(可选)
JUnit 使用包中的 Java Logging API java.util.logging(又名JUL)来发出警告和调试信息。LogManager配置选项请参考官方文档
。
或者,可以将日志消息重定向到其他日志框架,例如
Log4j或Logback。要使用提供自定义实现的日志记录框架
LogManager,请将java.util.logging.manager系统属性设置为要使用的实现的完全限定类名。LogManager下面的示例演示了如何配置 Log4j 2.x(有关详细信息,请参阅Log4j JDK 日志适配器)。
test {
systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
}
其他日志框架提供了不同的方法来重定向使用
java.util.logging. 例如,对于Logback ,您可以
通过向运行时类路径添加额外的依赖项来使用JUL 到 SLF4J 桥接器。
4.2.2. 梅文
从版本 2.22.0开始,Maven Surefire 和 Maven Failsafe
为在 JUnit 平台上执行测试提供本机支持。pom.xml项目中的文件
演示junit5-jupiter-starter-maven了如何使用 Maven Surefire 插件,并且可以作为配置 Maven 构建的起点。
|
使用 Maven Surefire/Failsafe 3.0.0-M4 或更高版本以避免互操作性问题
Maven Surefire/Failsafe 3.0.0-M4 引入了对将其使用的 JUnit 平台启动器版本与测试运行时类路径上找到的 JUnit 平台版本进行对齐的支持。因此,建议使用3.0.0-M4或更高版本,以避免互操作性问题。 或者,您可以将 JUnit 平台启动器的匹配版本的测试依赖项添加到 Maven 构建中,如下所示。
|
调整依赖版本
除非您使用定义了自己的依赖关系管理方式的 Spring Boot,否则建议使用 JUnit 平台 BOM 来对齐所有 JUnit 5 工件的版本。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.10.1</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 的测试的支持,请配置testJUnit Jupiter API 和 JUnit JupiterTestEngine实现的范围依赖项,如下所示。
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version> <!-- can be omitted when using the BOM -->
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
只要您配置 JUnit 4test和 JUnit Vintage
TestEngine实现的范围依赖项,Maven Surefire 和 Maven Failsafe 就可以与 Jupiter 测试一起运行基于 JUnit 4 的测试,如下所示。
<!-- ... -->
<dependencies>
<!-- ... -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.10.1</version> <!-- can be omitted when using the BOM -->
<scope>test</scope>
</dependency>
<!-- ... -->
</dependencies>
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
<!-- ... -->
按测试类名称过滤
Maven Surefire 插件将扫描其完全限定名称与以下模式匹配的测试类。
-
**/Test*.java -
**/*Test.java -
**/*Tests.java -
**/*TestCase.java
而且,它默认会排除所有嵌套类(包括静态成员类)。
但请注意,您可以通过
在文件中配置显式规则include来覆盖此默认行为。例如,要防止 Maven Surefire 排除静态成员类,您可以重写其排除规则,如下所示。excludepom.xml
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<excludes>
<exclude/>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
有关详细信息,请参阅 Maven Surefire 的测试文档的包含和排除。
按标签过滤
-
要包含标签或标签表达式,请使用
groups. -
要排除标签或标签表达式,请使用
excludedGroups.
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<groups>acceptance | !feature-a</groups>
<excludedGroups>integration, regression</excludedGroups>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
配置参数
您可以通过使用 Java 文件语法(如下所示)或通过文件声明
属性并提供键值对来设置 JUnit Platform配置参数以影响测试发现和执行。configurationParametersPropertiesjunit-platform.properties
<!-- ... -->
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.conditions.deactivate = *
junit.jupiter.extensions.autodetection.enabled = true
junit.jupiter.testinstance.lifecycle.default = per_class
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
4.2.3. 蚂蚁
从 version 开始1.10.3,Ant有一个
junitlauncher任务为在 JUnit 平台上启动测试提供本机支持。该junitlauncher
任务仅负责启动 JUnit 平台并向其传递选定的测试集合。然后,JUnit 平台委托注册的测试引擎来发现和执行测试。
该junitlauncher任务尝试尽可能与本机 Ant 构造(例如
资源集合)保持一致
,以允许用户选择他们希望由测试引擎执行的测试。与许多其他核心 Ant 任务相比,这使该任务具有一致和自然的感觉。
1.10.6从Ant版本开始,该junitlauncher任务支持
在单独的 JVM 中分叉测试。
build.xml项目中的文件演示junit5-jupiter-starter-ant了如何使用该任务并可以作为起点。
基本用法
以下示例演示如何配置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 平台的类路径。该类路径还将用于定位作为执行一部分的测试类。
以下示例演示如何配置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提供自动支持来管理项目中使用的 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,您可以通过在文件中包含以下内容来覆盖 JUnit Jupiter 版本
build.gradle。
ext['junit-jupiter.version'] = '5.10.1'
使用 Maven,您可以通过在文件中包含以下内容来覆盖 JUnit Jupiter 版本
pom.xml。
<properties>
<junit-jupiter.version>5.10.1</junit-jupiter.version>
</properties>
4.3. 控制台启动器
它ConsoleLauncher是一个命令行 Java 应用程序,可让您从控制台启动 JUnit 平台。例如,它可用于运行 JUnit Vintage 和 JUnit Jupiter 测试并将测试执行结果打印到控制台。
包含所有依赖项的可执行文件junit-platform-console-standalone-1.10.1.jar发布在Maven 中央存储库的 junit-platform-console-standalone目录下
。它包括以下依赖项:
-
junit:junit:4.13.2 -
org.apiguardian:apiguardian-api:1.1.2 -
org.hamcrest:hamcrest-core:1.3 -
org.junit.jupiter:junit-jupiter-api:5.10.1 -
org.junit.jupiter:junit-jupiter-engine:5.10.1 -
org.junit.jupiter:junit-jupiter-params:5.10.1 -
org.junit.platform:junit-platform-commons:1.10.1 -
org.junit.platform:junit-platform-console:1.10.1 -
org.junit.platform:junit-platform-engine:1.10.1 -
org.junit.platform:junit-platform-launcher:1.10.1 -
org.junit.platform:junit-platform-reporting:1.10.1 -
org.junit.platform:junit-platform-suite-api:1.10.1 -
org.junit.platform:junit-platform-suite-commons:1.10.1 -
org.junit.platform:junit-platform-suite-engine:1.10.1 -
org.junit.platform:junit-platform-suite:1.10.1 -
org.junit.vintage:junit-vintage-engine:5.10.1 -
org.opentest4j:opentest4j:1.3.0
您可以运行独立程序ConsoleLauncher,如下所示。
$ java -jar junit-platform-console-standalone-1.10.1.jar execute <OPTIONS>
├─ JUnit Vintage
│ └─ example.JUnit4Tests
│ └─ standardJUnit4Test ✔
└─ JUnit Jupiter
├─ StandardTests
│ ├─ succeedingTest() ✔
│ └─ skippedTest() ↷ for demonstration purposes
└─ A special test case
├─ Custom test name containing spaces ✔
├─ ╯°□°)╯ ✔
└─ ???? ✔
Test run finished after 64 ms
[ 5 containers found ]
[ 0 containers skipped ]
[ 5 containers started ]
[ 0 containers aborted ]
[ 5 containers successful ]
[ 0 containers failed ]
[ 6 tests found ]
[ 1 tests skipped ]
[ 5 tests started ]
[ 0 tests aborted ]
[ 5 tests successful ]
[ 0 tests failed ]
您还可以运行独立程序ConsoleLauncher,如下所示(例如,包含目录中的所有 jar):
$ java -cp classes:testlib/* org.junit.platform.console.ConsoleLauncher <OPTIONS>
|
退出代码 如果任何容器或测试失败,则
退出ConsoleLauncher并带有状态代码。1如果未发现任何测试并且--fail-if-no-tests提供了命令行选项,则ConsoleLauncher退出并显示状态代码2。否则,退出代码为0。
|
4.3.1. 子命令和选项
提供ConsoleLauncher以下子命令:
Usage: junit [OPTIONS] [COMMAND]
Launches the JUnit Platform for test discovery and execution.
[@<filename>...] One or more argument files containing options.
Commands:
discover Discover tests
execute Execute tests
engines List available test engines
For more information, please refer to the JUnit User Guide at
https://junit.org/junit5/docs/current/user-guide/
发现测试
Usage: junit discover [OPTIONS]
Discover tests
[@<filename>...] One or more argument files containing options.
--disable-banner Disable print out of the welcome message.
--disable-ansi-colors Disable ANSI colors in output (not supported by all terminals).
-h, --help Display help information.
SELECTORS
--scan-classpath, --scan-class-path[=PATH]
Scan all directories on the classpath or explicit classpath
roots. Without arguments, only directories on the system
classpath as well as additional classpath entries supplied via
-cp (directories and JAR files) are scanned. Explicit classpath
roots that are not on the classpath will be silently ignored.
This option can be repeated.
--scan-modules Scan all resolved modules for test discovery.
-u, --select-uri=URI Select a URI for test discovery. This option can be repeated.
-f, --select-file=FILE Select a file for test discovery. This option can be repeated.
-d, --select-directory=DIR Select a directory for test discovery. This option can be
repeated.
-o, --select-module=NAME Select single module for test discovery. This option can be
repeated.
-p, --select-package=PKG Select a package for test discovery. This option can be repeated.
-c, --select-class=CLASS Select a class for test discovery. This option can be repeated.
-m, --select-method=NAME Select a method for test discovery. This option can be repeated.
-r, --select-resource=RESOURCE
Select a classpath resource for test discovery. This option can
be repeated.
-i, --select-iteration=TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]
Select iterations for test discovery (e.g. method:com.acme.Foo#m()
[1..2]). This option can be repeated.
FILTERS
-n, --include-classname=PATTERN
Provide a regular expression to include only classes whose fully
qualified names match. To avoid loading classes unnecessarily,
the default pattern only includes class names that begin with
"Test" or end with "Test" or "Tests". When this option is
repeated, all patterns will be combined using OR semantics.
Default: ^(Test.*|.+[.$]Test.*|.*Tests?)$
-N, --exclude-classname=PATTERN
Provide a regular expression to exclude those classes whose fully
qualified names match. When this option is repeated, all
patterns will be combined using OR semantics.
--include-package=PKG Provide a package to be included in the test run. This option can
be repeated.
--exclude-package=PKG Provide a package to be excluded from the test run. This option
can be repeated.
-t, --include-tag=TAG Provide a tag or tag expression to include only tests whose tags
match. When this option is repeated, all patterns will be
combined using OR semantics.
-T, --exclude-tag=TAG Provide a tag or tag expression to exclude those tests whose tags
match. When this option is repeated, all patterns will be
combined using OR semantics.
-e, --include-engine=ID Provide the ID of an engine to be included in the test run. This
option can be repeated.
-E, --exclude-engine=ID Provide the ID of an engine to be excluded from the test run.
This option can be repeated.
RUNTIME CONFIGURATION
-cp, --classpath, --class-path=PATH
Provide additional classpath entries -- for example, for adding
engines and their dependencies. This option can be repeated.
--config=KEY=VALUE Set a configuration parameter for test discovery and execution.
This option can be repeated.
CONSOLE OUTPUT
--color-palette=FILE Specify a path to a properties file to customize ANSI style of
output (not supported by all terminals).
--single-color Style test output using only text attributes, no color (not
supported by all terminals).
--details=MODE Select an output details mode for when tests are executed. Use
one of: none, summary, flat, tree, verbose, testfeed. If 'none'
is selected, then only the summary and test failures are shown.
Default: tree.
--details-theme=THEME Select an output details tree theme for when tests are executed.
Use one of: ascii, unicode. Default is detected based on
default character encoding.
For more information, please refer to the JUnit User Guide at
https://junit.org/junit5/docs/current/user-guide/
执行测试
Usage: junit execute [OPTIONS]
Execute tests
[@<filename>...] One or more argument files containing options.
--disable-banner Disable print out of the welcome message.
--disable-ansi-colors Disable ANSI colors in output (not supported by all terminals).
-h, --help Display help information.
SELECTORS
--scan-classpath, --scan-class-path[=PATH]
Scan all directories on the classpath or explicit classpath
roots. Without arguments, only directories on the system
classpath as well as additional classpath entries supplied via
-cp (directories and JAR files) are scanned. Explicit classpath
roots that are not on the classpath will be silently ignored.
This option can be repeated.
--scan-modules Scan all resolved modules for test discovery.
-u, --select-uri=URI Select a URI for test discovery. This option can be repeated.
-f, --select-file=FILE Select a file for test discovery. This option can be repeated.
-d, --select-directory=DIR Select a directory for test discovery. This option can be
repeated.
-o, --select-module=NAME Select single module for test discovery. This option can be
repeated.
-p, --select-package=PKG Select a package for test discovery. This option can be repeated.
-c, --select-class=CLASS Select a class for test discovery. This option can be repeated.
-m, --select-method=NAME Select a method for test discovery. This option can be repeated.
-r, --select-resource=RESOURCE
Select a classpath resource for test discovery. This option can
be repeated.
-i, --select-iteration=TYPE:VALUE[INDEX(..INDEX)?(,INDEX(..INDEX)?)*]
Select iterations for test discovery (e.g. method:com.acme.Foo#m()
[1..2]). This option can be repeated.
FILTERS
-n, --include-classname=PATTERN
Provide a regular expression to include only classes whose fully
qualified names match. To avoid loading classes unnecessarily,
the default pattern only includes class names that begin with
"Test" or end with "Test" or "Tests". When this option is
repeated, all patterns will be combined using OR semantics.
Default: ^(Test.*|.+[.$]Test.*|.*Tests?)$
-N, --exclude-classname=PATTERN
Provide a regular expression to exclude those classes whose fully
qualified names match. When this option is repeated, all
patterns will be combined using OR semantics.
--include-package=PKG Provide a package to be included in the test run. This option can
be repeated.
--exclude-package=PKG Provide a package to be excluded from the test run. This option
can be repeated.
-t, --include-tag=TAG Provide a tag or tag expression to include only tests whose tags
match. When this option is repeated, all patterns will be
combined using OR semantics.
-T, --exclude-tag=TAG Provide a tag or tag expression to exclude those tests whose tags
match. When this option is repeated, all patterns will be
combined using OR semantics.
-e, --include-engine=ID Provide the ID of an engine to be included in the test run. This
option can be repeated.
-E, --exclude-engine=ID Provide the ID of an engine to be excluded from the test run.
This option can be repeated.
RUNTIME CONFIGURATION
-cp, --classpath, --class-path=PATH
Provide additional classpath entries -- for example, for adding
engines and their dependencies. This option can be repeated.
--config=KEY=VALUE Set a configuration parameter for test discovery and execution.
This option can be repeated.
CONSOLE OUTPUT
--color-palette=FILE Specify a path to a properties file to customize ANSI style of
output (not supported by all terminals).
--single-color Style test output using only text attributes, no color (not
supported by all terminals).
--details=MODE Select an output details mode for when tests are executed. Use
one of: none, summary, flat, tree, verbose, testfeed. If 'none'
is selected, then only the summary and test failures are shown.
Default: tree.
--details-theme=THEME Select an output details tree theme for when tests are executed.
Use one of: ascii, unicode. Default is detected based on
default character encoding.
REPORTING
--fail-if-no-tests Fail and return exit status code 2 if no tests are found.
--reports-dir=DIR Enable report output into a specified local directory (will be
created if it does not exist).
For more information, please refer to the JUnit User Guide at
https://junit.org/junit5/docs/current/user-guide/
列出测试引擎
Usage: junit engines [OPTIONS]
List available test engines
[@<filename>...] One or more argument files containing options.
--disable-banner Disable print out of the welcome message.
--disable-ansi-colors
Disable ANSI colors in output (not supported by all terminals).
-h, --help Display help information.
For more information, please refer to the JUnit User Guide at
https://junit.org/junit5/docs/current/user-guide/
4.3.2. 参数文件(@-文件)
在某些平台上,当创建具有大量选项或长参数的命令行时,您可能会遇到命令行长度的系统限制。
从 1.3 版本开始,ConsoleLauncher支持参数 files,也称为
@-files。参数文件是本身包含要传递给命令的参数的文件。当底层picocli命令行解析器遇到以字符 开头的参数时@,它会将该文件的内容扩展到参数列表中。
文件中的参数可以用空格或换行符分隔。如果参数包含嵌入的空格,则整个参数应该用双引号或单引号引起来 — 例如,"-f=My Files/Stuff.java"。
如果参数文件不存在或无法读取,则参数将按字面意思处理,不会被删除。这可能会导致“参数不匹配”错误消息。picocli.trace您可以通过在系统属性设置为 的情况下执行命令来解决此类错误
DEBUG。
可以在命令行上指定多个@-文件。指定的路径可以是相对于当前目录的路径,也可以是绝对路径。
@您可以通过使用附加符号对其进行转义来传递带有初始字符的实际参数@。例如,@@somearg将成为@somearg且不会受到扩张。
4.3.3. 颜色定制
输出中使用的颜色ConsoleLauncher可以自定义。该选项--single-color将应用内置的单色样式,同时
--color-palette接受属性文件来覆盖
ANSI SGR颜色样式。下面的属性文件演示了默认样式:
SUCCESSFUL = 32
ABORTED = 33
FAILED = 31
SKIPPED = 35
CONTAINER = 35
TEST = 34
DYNAMIC = 35
REPORTED = 37
4.4. 使用 JUnit 4 运行 JUnit 平台
|
跑步
JUnitPlatform者已被弃用该 近年来,所有主流构建工具和 IDE 都提供了直接在 JUnit 平台上运行测试的内置支持。 此外,模块 因此,运行 如果您正在使用 |
该JUnitPlatform运行器是基于 JUnit 4 的Runner,它使您能够在 JUnit 4 环境中的 JUnit 平台上运行其编程模型受支持的任何测试 - 例如,JUnit Jupiter 测试类。
使用 注释类@RunWith(JUnitPlatform.class)可以使其在 IDE 中运行并构建支持 JUnit 4 但尚不直接支持 JUnit 平台的系统。
| 由于 JUnit 平台具有 JUnit 4 所没有的功能,因此运行程序只能支持 JUnit 平台功能的子集,特别是在报告方面(请参阅显示名称与技术名称)。 |
4.4.1. 设置
您需要以下工件及其对类路径的依赖项。有关组 ID、工件 ID 和版本的详细信息,请参阅 依赖项元数据。
4.4.2. 显示名称与技术名称
要为类定义自定义显示名称,@RunWith(JUnitPlatform.class)请通过
注释类并@SuiteDisplayName提供自定义值来运行。
默认情况下,显示名称将用于测试工件;然而,当
JUnitPlatform运行器用于使用 Gradle 或 Maven 等构建工具执行测试时,生成的测试报告通常需要包含测试工件的技术名称(例如,完全限定的类名称),而不是像测试类的简单名称或包含特殊字符的自定义显示名称。要启用用于报告目的的技术名称,请
@UseTechnicalNames在 旁边声明注释@RunWith(JUnitPlatform.class)。
请注意, 的存在@UseTechnicalNames会覆盖通过 配置的任何自定义显示名称@SuiteDisplayName。
4.4.3. 单一测试班
使用JUnitPlatform运行器的一种方法是直接使用注释测试类
@RunWith(JUnitPlatform.class)。org.junit.jupiter.api.Test请注意,以下示例中的测试方法用(JUnit Jupiter)
注释,而不是org.junit.Test(JUnit 4)。此外,在这种情况下,测试类必须是public;否则,某些 IDE 和构建工具可能无法将其识别为 JUnit 4 测试类。
import static org.junit.jupiter.api.Assertions.fail;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
@RunWith(org.junit.platform.runner.JUnitPlatform.class)
public class JUnitPlatformClassDemo {
@Test
void succeedingTest() {
/* no-op */
}
@Test
void failingTest() {
fail("Failing for failing's sake.");
}
}
4.4.4. 测试套件
如果您有多个测试类,您可以创建一个测试套件,如以下示例所示。
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.SuiteDisplayName;
import org.junit.runner.RunWith;
@RunWith(org.junit.platform.runner.JUnitPlatform.class)
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
public class JUnitPlatformSuiteDemo {
}
将发现并运行包及其子包JUnitPlatformSuiteDemo中的所有测试。example默认情况下,它只包含名称以 or 开头或Test结尾的测试类。TestTests
|
附加配置选项
有更多的配置选项用于发现和过滤测试,而不仅仅是
@SelectPackages. 请参阅包的 Javadocorg.junit.platform.suite.api了解更多详细信息。
|
注释的测试类和套件@RunWith(JUnitPlatform.class)
不能直接在 JUnit 平台上执行(或作为某些 IDE 中记录的“JUnit 5”测试)。此类类和套件只能使用 JUnit 4 基础设施来执行。
|
4.5. 配置参数
除了指示平台要包含哪些测试类和测试引擎、要扫描哪些包等之外,有时还需要提供特定于特定测试引擎、侦听器或注册扩展的其他自定义配置参数。例如,JUnit JupiterTestEngine支持以下用例的配置参数。
配置参数是基于文本的键值对,可以通过以下机制之一提供给在 JUnit 平台上运行的测试引擎。
-
其中的 和 方法 用于
configurationParameter()构建提供给API 的请求 。通过 JUnit 平台提供的工具之一运行测试时,您可以指定配置参数,如下所示:configurationParameters()LauncherDiscoveryRequestBuilderLauncher-
控制台启动器:使用
--config命令行选项。 -
Gradle:使用
systemProperty或systemPropertiesDSL。 -
Maven Surefire 提供者:使用该
configurationParameters属性。
-
-
JVM 系统属性。
-
junit-platform.propertiesJUnit Platform 配置文件:在类路径根目录中命名的文件,遵循 JavaProperties文件的语法规则。
配置参数按照上面定义的确切顺序查找。因此,直接提供的配置参数优先Launcher于通过系统属性和配置文件提供的配置参数。同样,通过系统属性提供的配置参数优先于通过配置文件提供的配置参数。
|
4.5.1. 模式匹配语法
本节介绍应用于以下功能的配置参数的模式匹配语法。
如果给定配置参数的值仅包含星号 ( *),则该模式将与所有候选类匹配。否则,该值将被视为以逗号分隔的模式列表,其中每个模式将与每个候选类的完全限定类名 ( FQCN ) 进行匹配。模式中的任何点 ( ) 都将与FQCN 中的.点 ( .) 或美元符号 ( ) 匹配。$任何星号 ( *) 都将与 FQCN 中的一个或多个字符匹配。模式中的所有其他字符将与 FQCN 一对一匹配。
例子:
-
*:匹配所有候选类别。 -
org.junit.*org.junit:匹配基础包及其任何子包下的所有候选类。 -
*.MyCustomImpl:匹配简单类名恰好为 的每个候选类MyCustomImpl。 -
*System*:匹配 FQCN 包含 的每个候选类System。 -
*System*, *Unit*System:匹配 FQCN 包含或 的每个候选类Unit。 -
org.example.MyCustomImpl:匹配 FQCN 恰好为 的候选类org.example.MyCustomImpl。 -
org.example.MyCustomImpl, org.example.TheirCustomImplorg.example.MyCustomImpl:匹配 FQCN 恰好为 或的候选类org.example.TheirCustomImpl。
4.6. 标签
标签是用于标记和过滤测试的 JUnit 平台概念。向容器和测试添加标签的编程模型由测试框架定义。例如,在基于 JUnit Jupiter 的测试中,应使用@Tag注释(请参阅
标记和过滤)。对于基于 JUnit 4 的测试,Vintage 引擎将@Category注释映射到标签(请参阅
类别支持)。其他测试框架可能会定义自己的注释或其他方式供用户指定标签。
4.6.1. 标签的语法规则
无论如何指定标签,JUnit 平台都会强制执行以下规则:
-
标签不能为
null或 为空。 -
修剪后的标签不得包含空格。
-
修剪后的标签不得包含 ISO 控制字符。
-
修剪后的标记不得包含以下任何保留字符。
-
,:逗号 -
(:左括号 -
):右括号 -
&:和符号 -
|:竖线 -
!:感叹号
-
| 在上面的上下文中,“修剪”意味着前导和尾随空白字符已被删除。 |
4.6.2. 标签表达式
标签表达式是带有运算符!,&和的布尔表达式|。此外,
(和)可用于调整运算符优先级。
支持两个特殊表达式any()和none(),分别选择带有
任何标签的所有测试和不带任何标签的所有测试。这些特殊表达式可以像普通标签一样与其他表达式组合。
| 操作员 | 意义 | 关联性 |
|---|---|---|
|
不是 |
正确的 |
|
和 |
左边 |
|
或者 |
左边 |
如果您跨多个维度标记测试,标记表达式可帮助您选择要执行的测试。当按测试类型(例如,微型、集成、 端到端)和功能(例如,产品、目录、运输)进行标记时,以下标记表达式可能很有用。
| 标签表达 | 选择 |
|---|---|
|
产品的所有测试 |
|
所有目录测试以及所有运输测试 |
|
目录和运输之间交叉的所有测试 |
|
产品的所有测试,但不是端到端测试 |
|
产品或运输的所有微观或集成测试 |
4.7. 捕获标准输出/错误
System.out从版本 1.3 开始,JUnit 平台提供了对捕获打印到和 的输出的选择性支持System.err。要启用它,请将
junit.platform.output.capture.stdout和/或junit.platform.output.capture.stderr
配置参数设置为true。此外,您可以使用 来配置每个执行的测试或容器使用的最大缓冲字节数junit.platform.output.capture.maxBuffer。
如果启用,JUnit 平台会捕获相应的输出,并在报告测试或容器已完成之前立即使用stdout或stderr键将其发布为所有已注册
实例的报告条目。TestExecutionListener
请注意,捕获的输出将仅包含用于执行容器或测试的线程发出的输出。其他线程的任何输出都将被忽略,因为特别是在 并行执行测试时,不可能将其归因于特定的测试或容器。
4.8. 使用监听器和拦截器
JUnit 平台提供以下侦听器 API,允许 JUnit、第三方和自定义用户代码对发现和执行TestPlan.
-
LauncherSessionListenerLauncherSession:a打开和关闭时接收事件。 -
LauncherInterceptor:在 . 的上下文中拦截测试发现和执行LauncherSession。 -
LauncherDiscoveryListener:接收测试发现期间发生的事件。 -
TestExecutionListener:接收测试执行期间发生的事件。
APILauncherSessionListener通常由构建工具或 IDE 实现,并自动为您注册,以支持构建工具或 IDE 的某些功能。
和API 的实现通常是为了生成某种形式的报告或在 IDE 中显示测试计划的图形表示LauncherDiscoveryListener。TestExecutionListener此类侦听器可能由构建工具或 IDE 实现并自动注册,或者它们可能包含在第三方库中 - 可能会自动为您注册。您还可以实现并注册您自己的侦听器。
有关注册和配置侦听器的详细信息,请参阅本指南的以下部分。
JUnit 平台提供了以下侦听器,您可能希望将它们与测试套件一起使用。
- JUnit 平台报告
-
LegacyXmlReportGeneratingListener可以通过控制台启动器使用 或手动注册来生成与基于 JUnit 4 的测试报告的事实标准兼容的 XML 报告。OpenTestReportGeneratingListener以Open Test Reporting指定的基于事件的格式生成 XML 报告。它是自动注册的,可以通过配置参数启用和配置。有关详细信息,请参阅JUnit 平台报告。
- 飞行记录仪支持
-
FlightRecordingExecutionListener并FlightRecordingDiscoveryListener在测试发现和执行期间生成 Java Flight Recorder 事件。 LoggingListener-
TestExecutionListenerBiConsumer用于通过使用Throwable和 的来记录所有事件的信息消息Supplier<String>。 SummaryGeneratingListener-
TestExecutionListener生成测试执行的摘要,可以通过PrintWriter. UniqueIdTrackingListener-
TestExecutionListener它跟踪在执行期间跳过或执行的所有测试的唯一 ID ,并在执行完成TestPlan后生成包含唯一 ID 的文件。TestPlan
4.8.1. 飞行记录仪支持
从版本 1.7 开始,JUnit 平台提供了对生成 Flight Recorder 事件的选择支持。JEP 328将 Java 飞行记录器 (JFR) 描述为:
| Flight Recorder 记录源自应用程序、JVM 和操作系统的事件。事件存储在单个文件中,该文件可以附加到错误报告中并由支持工程师进行检查,从而可以对问题发生期间的问题进行事后分析。 |
为了记录运行测试时生成的 Flight Recorder 事件,您需要:
-
确保您使用的是 Java 8 Update 262 或更高版本或者 Java 11 或更高版本。
-
在测试运行时在类路径或模块路径上提供
org.junit.platform.jfr模块 ( )。junit-platform-jfr-1.10.1.jar -
启动试运行时开始飞行记录。可以通过 java 命令行选项启动 Flight Recorder:
-XX:StartFlightRecording:filename=...
请查阅构建工具的手册以获取适当的命令。
要分析记录的事件,请使用 最新 JDK 附带的 jfr命令行工具或使用JDK Mission Control打开记录文件。
| 飞行记录器支持目前是一项实验性功能。我们邀请您尝试一下并向 JUnit 团队提供反馈,以便他们改进并最终 推广此功能。 |
4.9. 堆栈跟踪修剪
从版本 1.10 开始,JUnit 平台提供了对修剪失败测试所产生的堆栈跟踪的内置支持。该功能默认启用,但可以通过将junit.platform.stacktrace.pruning.enabled 配置参数设置为 来
禁用false。
启用后,来自org.junit、jdk.internal.reflect和sun.reflect
包的所有调用都会从堆栈跟踪中删除,除非调用发生在测试本身或其任何祖先之后。因此,永远不会排除拨打org.junit.jupiter.api.Assertions或 的电话。org.junit.jupiter.api.Assumptions
此外,JUnit 平台启动器第一次调用之前的所有元素(包括 JUnit 平台启动器的第一次调用)都将被删除。
5. 扩展模型
5.1. 概述
与 JUnit 4 中竞争的Runner、TestRule、 和MethodRule扩展点相比,JUnit Jupiter 扩展模型由一个单一的、连贯的概念组成:API
Extension。但请注意,它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生成随机数,这些随机数可以注入到字段中或通过构造函数、测试方法或生命周期方法中的参数注入。如果扩展提供了@Random元注释的注释
@ExtendWith(RandomNumberExtension.class)(请参阅下面的列表),则可以透明地使用扩展,如下RandomNumberDemo例所示。
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
class RandomNumberDemo {
// Use static randomNumber0 field anywhere in the test class,
// including @BeforeAll or @AfterEach lifecycle methods.
@Random
private static Integer randomNumber0;
// Use randomNumber1 field in test methods and @BeforeEach
// or @AfterEach lifecycle methods.
@Random
private int randomNumber1;
RandomNumberDemo(@Random int randomNumber2) {
// Use randomNumber2 in constructor.
}
@BeforeEach
void beforeEach(@Random int randomNumber3) {
// Use randomNumber3 in @BeforeEach method.
}
@Test
void test(@Random int randomNumber4) {
// Use randomNumber4 in test method.
}
}
下面的代码清单提供了一个示例,说明人们可以如何选择实现这样的
RandomNumberExtension. 此实现适用于以下用例
RandomNumberDemo:然而,它可能不足以覆盖所有用例——例如,随机数生成支持仅限于整数;它使用
java.util.Random而不是java.security.SecureRandom; 无论如何,重要的是要注意实现了哪些扩展 API 以及实现的原因。
具体来说,RandomNumberExtension实现了以下扩展API:
-
BeforeAllCallback:支持静态场注入 -
BeforeEachCallback:支持非静态场注入 -
ParameterResolver:支持构造函数和方法注入
|
理想情况下,应该在测试类实例化后立即 但是,JUnit Jupiter 目前不允许在非静态字段上
|
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedFields;
import java.lang.reflect.Field;
import java.util.function.Predicate;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.ModifierSupport;
class RandomNumberExtension
implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {
private final java.util.Random random = new java.util.Random(System.nanoTime());
/**
* Inject a random integer into static fields that are annotated with
* {@code @Random} and can be assigned an integer value.
*/
@Override
public void beforeAll(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
injectFields(testClass, null, ModifierSupport::isStatic);
}
/**
* Inject a random integer into non-static fields that are annotated with
* {@code @Random} and can be assigned an integer value.
*/
@Override
public void beforeEach(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
Object testInstance = context.getRequiredTestInstance();
injectFields(testClass, testInstance, ModifierSupport::isNotStatic);
}
/**
* Determine if the parameter is annotated with {@code @Random} and can be
* assigned an integer value.
*/
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
return pc.isAnnotated(Random.class) && isInteger(pc.getParameter().getType());
}
/**
* Resolve a random integer.
*/
@Override
public Integer resolveParameter(ParameterContext pc, ExtensionContext ec) {
return this.random.nextInt();
}
private void injectFields(Class<?> testClass, Object testInstance,
Predicate<Field> predicate) {
predicate = predicate.and(field -> isInteger(field.getType()));
findAnnotatedFields(testClass, Random.class, predicate)
.forEach(field -> {
try {
field.setAccessible(true);
field.set(testInstance, this.random.nextInt());
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
});
}
private static boolean isInteger(Class<?> type) {
return type == Integer.class || type == int.class;
}
}
@ExtendWith字段的扩展注册顺序通过 on 字段以声明方式注册的扩展将使用确定性但故意不明显的算法 |
5.2.2. 程序化扩展注册
开发人员可以通过用@RegisterExtension.
当通过
声明性注册扩展时@ExtendWith,它通常只能通过注释进行配置。相反,当通过 注册扩展时,可以通过编程方式@RegisterExtension对其进行配置 - 例如,以便将参数传递给扩展的构造函数、静态工厂方法或构建器 API。
|
延期登记令
默认情况下,通过字段以编程方式 任何 |
@RegisterExtension字段不得null(在评估时)但可以是静态的static或非静态的。
|
静态字段
如果@RegisterExtension字段是static,则扩展将在通过在类级别注册的扩展之后注册@ExtendWith。此类静态扩展不受其可以实现的扩展 API 的限制。因此,通过静态字段注册的扩展可以实现类级和实例级扩展 API,例如BeforeAllCallback、AfterAllCallback、
TestInstancePostProcessor和TestInstancePreDestroyCallback以及方法级扩展 API BeforeEachCallback,例如 等。
在以下示例中,server测试类中的字段是使用WebServerExtension. 配置的内容WebServerExtension将自动注册为类级别的扩展 - 例如,以便在类中的所有测试之前启动服务器,然后在类中的所有测试完成后停止服务器。此外,如果需要,用@BeforeAll或注解的静态生命周期方法@AfterAll以及@BeforeEach、
@AfterEach和@Test方法可以通过该字段访问扩展的实例
。server
class WebServerDemo {
@RegisterExtension
static WebServerExtension server = WebServerExtension.builder()
.enableSecurity(false)
.build();
@Test
void getProductList() {
WebClient webClient = new WebClient();
String serverUrl = server.getServerUrl();
// Use WebClient to connect to web server using serverUrl and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
}
Kotlin 中的静态字段
Kotlin 编程语言没有字段的概念static。private static但是,可以指示编译器使用Kotlin 中的注释生成字段@JvmStatic
。如果您希望 Kotlin 编译器生成public static字段,则可以使用@JvmField注释来代替。
WebServerDemo以下示例是上一节中已移植到 Kotlin的版本。
class KotlinWebServerDemo {
companion object {
@JvmStatic
@RegisterExtension
val server = WebServerExtension.builder()
.enableSecurity(false)
.build()
}
@Test
fun getProductList() {
// Use WebClient to connect to web server using serverUrl and verify response
val webClient = WebClient()
val serverUrl = server.serverUrl
assertEquals(200, webClient.get("$serverUrl/products").responseStatus)
}
}
实例字段
如果@RegisterExtension字段是非静态的(即实例字段),则扩展将在测试类实例化之后注册,并且在每个注册都有机会后处理
TestInstancePostProcessor测试实例之后(可能注入测试类的实例)用于注释字段的扩展名)。因此,如果此类实例扩展实现了类级或实例级扩展 API(例如BeforeAllCallback、AfterAllCallback或 )
TestInstancePostProcessor,则这些 API 将不会得到支持。默认情况下,实例扩展将在通过 ; 在方法级别注册的扩展之后@ExtendWith注册。但是,如果测试类配置了
语义,则实例扩展将在通过 . 在方法级别注册的扩展之前@TestInstance(Lifecycle.PER_CLASS)注册
。@ExtendWith
在以下示例中,docs测试类中的字段通过调用自定义方法lookUpDocsDir()并将结果提供给. 配置的
将自动注册为方法级别的扩展。此外,如有必要, 、、 和方法可以通过字段访问扩展的实例。forPath()DocumentationExtensionDocumentationExtension@BeforeEach@AfterEach@Testdocs
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 文件的文件夹org.junit.jupiter.api.extension.Extension内
命名的文件中提供其完全限定类名来注册自定义扩展。/META-INF/services
启用自动扩展检测
自动检测是一项高级功能,因此默认情况下不启用。要启用它,请将junit.jupiter.extensions.autodetection.enabled 配置参数设置为
true。这可以作为 JVM 系统属性、作为传递到 的配置参数提供,或者通过 JUnit 平台配置文件提供(有关详细信息,请参阅配置参数)。LauncherDiscoveryRequestLauncher
例如,要启用扩展的自动检测,您可以使用以下系统属性启动 JVM。
-Djunit.jupiter.extensions.autodetection.enabled=true
当启用自动检测时,通过该ServiceLoader机制发现的扩展将被添加到扩展注册表中的 JUnit Jupiter 全局扩展之后(例如,支持TestInfo、TestReporter等)。
5.3. 条件测试执行
ExecutionCondition定义Extension用于编程、条件测试执行的 API 。
ExecutionCondition对每个容器(例如,测试类)评估an以确定是否应根据提供的 执行它包含的所有测试
ExtensionContext。类似地,对每个测试ExecutionCondition进行评估,以确定是否应根据提供的 执行给定的测试方法
ExtensionContext。
当注册多个ExecutionCondition扩展时,一旦其中一个条件返回disabled ,容器或测试就会被禁用。因此,无法保证评估条件,因为另一个扩展可能已经导致容器或测试被禁用。换句话说,评估的工作方式类似于短路布尔 OR 运算符。
具体示例请参见DisabledCondition和的源代码。@Disabled
5.3.1. 停用条件
有时,在某些条件未激活的情况下运行测试套件可能会很有用。例如,您可能希望运行测试,即使它们带有注释,@Disabled以便查看它们是否仍然损坏。为此,请为
junit.jupiter.conditions.deactivate 配置参数提供一个模式,以指定当前测试运行应停用(即不评估)哪些条件。该模式可以作为 JVM 系统属性、作为
传递给 的配置参数提供,或者通过 JUnit 平台配置文件提供(有关详细信息,请参阅配置参数)。LauncherDiscoveryRequestLauncher
例如,要停用 JUnit 的@Disabled条件,您可以使用以下系统属性启动 JVM。
-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition
模式匹配语法
有关详细信息,请参阅模式匹配语法。
5.4. 测试实例预构造回调
TestInstancePreConstructCallback定义希望在构建测试实例之前
调用Extensions的
API (通过构造函数调用或 via )。TestInstanceFactory
此扩展提供对称调用,TestInstancePreDestroyCallback并与其他扩展结合使用以准备构造函数参数或跟踪测试实例及其生命周期。
5.5. 测试实例工厂
TestInstanceFactoryExtensions为希望创建测试类实例的API 定义。
常见用例包括从依赖项注入框架获取测试实例或调用静态工厂方法来创建测试类实例。
如果没有TestInstanceFactory注册,框架将调用
测试类的唯一ParameterResolver构造函数来实例化它,可能通过注册的扩展来解析构造函数参数。
实现的扩展TestInstanceFactory可以在测试接口、顶级测试类或@Nested测试类上注册。
|
注册为任何单个类实现的多个扩展 |
5.6. 测试实例后处理
TestInstancePostProcessorExtensions为希望发布流程测试实例的API 定义。
常见用例包括将依赖项注入测试实例、在测试实例上调用自定义初始化方法等。
有关具体示例,请参阅MockitoExtension和 的
源代码SpringExtension。
5.7. 测试实例预销毁回调
TestInstancePreDestroyCallback定义了希望在测试中使用测试实例之后和销毁测试实例之前Extensions处理测试实例的 API 。
常见用例包括清理已注入测试实例的依赖项、在测试实例上调用自定义的反初始化方法等。
5.8. 参数分辨率
ParameterResolver定义Extension用于在运行时动态解析参数的 API。
如果测试类构造函数、测试方法或生命周期方法(请参阅
定义)声明了一个参数,则该参数必须在运行时由ParameterResolver. AParameterResolver可以是内置的(请参阅
TestInfoParameterResolver)或由用户注册。一般来说,参数可以通过名称、类型、注释或其任意组合来解析。
如果您希望实现ParameterResolver仅根据参数类型解析参数的自定义,您可能会发现扩展它
TypeBasedParameterResolver作为此类用例的通用适配器很方便。
CustomTypeParameterResolver有关具体示例,请参阅、
CustomAnnotationParameterResolver和的源代码MapOfListsTypeBasedParameterResolver。
|
由于 JDK 9 之前的 JDK 版本生成的字节码中存在错误,对于内部类构造函数(例如,测试类中的构造函数
),
|
|
其他扩展还可以利用注册的 |
5.9. 测试结果处理
TestWatcher为希望处理测试方法执行结果的扩展定义 API 。具体来说,TestWatcher将使用以下事件的上下文信息来调用 a。
-
testDisabled:跳过禁用的测试方法后调用 -
testSuccessful:测试方法成功完成后调用 -
testAborted:测试方法中止后调用 -
testFailed:测试方法失败后调用
与定义
中提出的“测试方法”的定义相反
,在本文中测试方法是指任何@Test方法或@TestTemplate方法(例如,a@RepeatedTest或@ParameterizedTest)。
|
实现此接口的扩展可以在类级别、实例级别或方法级别注册。当在类级别注册时,将为任何包含的测试方法(包括类中的测试方法)TestWatcher调用a 。当在方法级别注册时,只会针对
其注册的测试方法调用 a。@NestedTestWatcher
|
如果 a 为了确保为给定类中的所有测试方法 |
如果类级别出现故障(例如,方法抛出异常),则
@BeforeAll不会报告任何测试结果。ExecutionCondition同样,如果通过(例如)禁用测试类,@Disabled则不会报告测试结果。
与其他扩展 API 相比,aTestWatcher不允许对测试的执行产生不利影响。因此,API 中的方法抛出的任何异常都
TestWatcher将记录在该WARNING级别,并且不允许传播或使测试执行失败。
|
在调用 API中的方法之前 |
5.10. 测试生命周期回调
以下接口定义了用于在测试执行生命周期的各个点扩展测试的 API。请参阅以下部分的示例以及包中每个接口的 Javadocorg.junit.jupiter.api.extension以了解更多详细信息。
|
实现多个扩展 API
扩展开发人员可以选择在单个扩展中实现任意数量的这些接口。SpringExtension具体示例
请参阅 的源代码。 |
5.10.1. 测试执行回调之前和之后
BeforeTestExecutionCallback并为希望添加将分别在执行测试方法之前和
之后立即执行的行为的AfterTestExecutionCallbackAPI 定义
。因此,这些回调非常适合计时、跟踪和类似的用例。如果您需要实现围绕和方法调用的回调,请实现
和。Extensions @BeforeEach@AfterEachBeforeEachCallbackAfterEachCallback
以下示例演示如何使用这些回调来计算和记录测试方法的执行时间。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注册了TimingExtensionvia @ExtendWith,因此其测试在执行时将应用此计时。
@ExtendWith(TimingExtension.class)
class TimingExtensionTests {
@Test
void sleep20ms() throws Exception {
Thread.sleep(20);
}
@Test
void sleep50ms() throws Exception {
Thread.sleep(50);
}
}
TimingExtensionTests以下是运行时生成的日志记录的示例。
INFO: Method [sleep20ms] took 24 ms. INFO: Method [sleep50ms] took 53 ms.
5.11. 异常处理
在测试执行期间抛出的异常可以在进一步传播之前被拦截并进行相应处理,以便可以在专门的Extensions. JUnit Jupiter 为Extensions希望通过 处理@Test方法期间抛出的异常以及通过测试生命周期方法( 、和
)TestExecutionExceptionHandler
期间抛出的异常提供 API 。@BeforeAll@BeforeEach@AfterEach@AfterAllLifecycleMethodExecutionExceptionHandler
以下示例显示了一个扩展,它将吞掉 的所有实例,IOException
但重新抛出任何其他类型的异常。
public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
throws Throwable {
if (throwable instanceof IOException) {
return;
}
throw throwable;
}
}
另一个示例展示了如何在设置和清理过程中抛出意外异常时准确记录被测应用程序的状态。请注意,与依赖生命周期回调(根据测试状态可能执行也可能不执行)不同,此解决方案保证在失败@BeforeAll、
@BeforeEach或@AfterEach后立即执行@AfterAll。
class RecordStateOnErrorExtension implements LifecycleMethodExecutionExceptionHandler {
@Override
public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during class setup");
throw ex;
}
@Override
public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during test setup");
throw ex;
}
@Override
public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during test cleanup");
throw ex;
}
@Override
public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during class cleanup");
throw ex;
}
}
可以按声明顺序为同一生命周期方法调用多个执行异常处理程序。如果其中一个处理程序吞下了已处理的异常,则后续处理程序将不会被执行,并且不会将任何故障传播到 JUnit 引擎,就好像该异常从未引发过一样。处理程序还可以选择重新抛出异常或抛出不同的异常,可能会包装原始异常。
扩展实现LifecycleMethodExecutionExceptionHandler希望处理在类级别上抛出的异常@BeforeAll或@AfterAll需要在类级别上注册,而BeforeEach和 的处理程序AfterEach也可以为单个测试方法注册。
// Register handlers for @Test, @BeforeEach, @AfterEach as well as @BeforeAll and @AfterAll
@ExtendWith(ThirdExecutedHandler.class)
class MultipleHandlersTestCase {
// Register handlers for @Test, @BeforeEach, @AfterEach only
@ExtendWith(SecondExecutedHandler.class)
@ExtendWith(FirstExecutedHandler.class)
@Test
void testMethod() {
}
}
5.12. 拦截调用
InvocationInterceptorExtensions为希望拦截对测试代码的调用定义 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;
}
}
}
5.13。为测试模板提供调用上下文
一种@TestTemplate方法只有在至少
TestTemplateInvocationContextProvider注册了一个方法后才能执行。每个这样的提供者负责提供Stream实例TestTemplateInvocationContext。每个上下文可以指定一个自定义显示名称和一个仅用于该@TestTemplate方法的下一次调用的附加扩展列表。
以下示例展示了如何编写测试模板以及如何注册和实现TestTemplateInvocationContextProvider.
final List<String> fruits = Arrays.asList("apple", "banana", "lemon");
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String fruit) {
assertTrue(fruits.contains(fruit));
}
public class MyTestTemplateInvocationContextProvider
implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext context) {
return Stream.of(invocationContext("apple"), invocationContext("banana"));
}
private TestTemplateInvocationContext invocationContext(String parameter) {
return new TestTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return parameter;
}
@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new ParameterResolver() {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(String.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameter;
}
});
}
};
}
}
在此示例中,测试模板将被调用两次。调用的显示名称将由调用上下文指定apple。banana每次调用都会注册一个ParameterResolver用于解析方法参数的自定义。使用时的输出ConsoleLauncher如下。
└─ testTemplate(String) ✔ ├─ apple ✔ └─ banana ✔
扩展TestTemplateInvocationContextProviderAPI 主要用于实现不同类型的测试,这些测试依赖于在不同上下文中重复调用类似测试的方法 - 例如,使用不同的参数,通过不同的方式准备测试类实例,或者在不修改上下文的情况下多次调用。请参阅重复测试或
参数化测试的实现,它们使用此扩展点来提供其功能。
5.14。在扩展中保持状态
通常,扩展仅实例化一次。因此,问题变得相关:如何从一次扩展调用到下一次调用保持状态?API
正是为此目的而ExtensionContext提供的。Store扩展可以将值放入存储中以供以后检索。有关将与方法级作用域一起TimingExtension使用的示例,请参阅
。Store重要的是要记住,在ExtensionContext测试执行期间存储在 中的值在周围的 中不可用ExtensionContext。由于ExtensionContexts可以嵌套,内部上下文的范围也可能受到限制。有关可用于通过Store.
ExtensionContext.Store.CloseableResourceCloseableResource其方法来通知。close() |
5.15。扩展中支持的实用程序
该junit-platform-commons工件公开了一个名为的包
org.junit.platform.commons.support,其中包含用于处理注释、类、反射和类路径扫描任务的维护实用程序方法。TestEngine鼓励
Extension作者使用这些受支持的方法,以便与 JUnit 平台的行为保持一致。
5.15.1. 注释支持
AnnotationSupport提供对带注释的元素(例如,包、注释、类、接口、构造函数、方法和字段)进行操作的静态实用方法。其中包括检查元素是否使用特定注释进行注释或元注释、搜索特定注释以及查找类或接口中带注释的方法和字段的方法。其中一些方法在实现的接口和类层次结构中搜索以查找注释。AnnotationSupport有关更多详细信息,请参阅 Javadoc
。
5.15.2. 班级支持
ClassSupport提供用于处理类(即 的实例java.lang.Class)的静态实用方法。ClassSupport有关更多详细信息,请参阅 Javadoc 。
5.15.3。反射支持
ReflectionSupport提供静态实用方法来增强标准 JDK 反射和类加载机制。其中包括扫描类路径以搜索与指定谓词匹配的类、加载和创建类的新实例以及查找和调用方法的方法。其中一些方法遍历类层次结构以查找匹配的方法。ReflectionSupport有关更多详细信息,请参阅 Javadoc 。
5.15.4。修改器支持
ModifierSupport提供用于使用成员和类修饰符的静态实用方法 — 例如,确定成员是否声明为public、private、
abstract、等。有关更多详细信息,static请参阅 Javadoc 。ModifierSupport
5.16。用户代码和扩展的相对执行顺序
当执行包含一个或多个测试方法的测试类时,除了用户提供的测试和生命周期方法之外,还会调用许多扩展回调。
| 另请参阅:测试执行顺序 |
5.16.1. 用户和分机代码
下图说明了用户提供的代码和扩展代码的相对顺序。用户提供的测试和生命周期方法以橙色显示,由扩展实现的回调代码以蓝色显示。灰色框表示执行单个测试方法,并且将对测试类中的每个测试方法重复执行。
下表进一步解释了 用户代码和分机代码图中的十六个步骤。
| 步 | 接口/注释 | 描述 |
|---|---|---|
1 |
界面 |
在执行容器的所有测试之前执行的扩展代码 |
2 |
注解 |
在执行容器的所有测试之前执行的用户代码 |
3 |
界面 |
用于处理 |
4 |
界面 |
在执行每个测试之前执行的扩展代码 |
5 |
注解 |
在执行每个测试之前执行的用户代码 |
6 |
界面 |
用于处理 |
7 |
界面 |
在执行测试之前立即执行扩展代码 |
8 |
注解 |
实际测试方法的用户代码 |
9 |
界面 |
用于处理测试期间抛出的异常的扩展代码 |
10 |
界面 |
测试执行后立即执行的扩展代码及其相应的异常处理程序 |
11 |
注解 |
每次测试执行后执行的用户代码 |
12 |
界面 |
用于处理 |
13 |
界面 |
每次测试执行后执行的扩展代码 |
14 |
注解 |
执行容器的所有测试后执行的用户代码 |
15 |
界面 |
用于处理 |
16 |
界面 |
执行容器的所有测试后执行的扩展代码 |
在最简单的情况下,仅执行实际的测试方法(步骤 8);所有其他步骤都是可选的,具体取决于用户代码是否存在或对相应生命周期回调的扩展支持。有关各种生命周期回调的更多详细信息,请参阅每个注释和扩展的相应 Javadoc。
上表中用户代码方法的所有调用都可以通过实现来拦截InvocationInterceptor。
5.16.2. 回调的包装行为
JUnit Jupiter 始终保证实现生命周期回调(例如、、
、、和 )
的多个注册扩展的包装行为。BeforeAllCallbackAfterAllCallbackBeforeEachCallbackAfterEachCallbackBeforeTestExecutionCallbackAfterTestExecutionCallback
这意味着,给定两个扩展Extension1并Extension2在Extension1
before 注册Extension2,任何由 实现的“之前”回调Extension1都保证在由 实现的任何“之前”回调之前Extension2执行。类似地,给定以相同顺序注册的两个相同的扩展,任何由 实现的“之后”回调Extension1都保证在由实现的任何“之后”回调之后执行Extension2。Extension1因此被称为换行
Extension2。
JUnit Jupiter 还保证用户提供的生命周期方法在类和接口层次结构中的包装行为(请参阅定义)。
-
@BeforeAll只要方法未被隐藏、 重写或取代(即,仅基于签名进行替换,而不考虑 Java 的可见性规则),方法就会从超类继承。此外,超类中的方法将在子类中的方法之前@BeforeAll执行。@BeforeAll-
同样,
@BeforeAll接口中声明的方法只要未被隐藏或覆盖,就会被继承,并且接口中的方法将在实现该接口的类中的方法之前@BeforeAll执行。@BeforeAll
-
-
@AfterAll只要方法未被隐藏、 重写或取代(即,仅基于签名进行替换,而不考虑 Java 的可见性规则),方法就会从超类继承。此外,超类中的方法将在子类中的方法之后@AfterAll执行。@AfterAll-
同样,
@AfterAll接口中声明的方法只要未被隐藏或覆盖,就会被继承,并且接口中的方法将在实现该接口的类中的方法之后@AfterAll执行。@AfterAll
-
-
@BeforeEach只要方法未被 覆盖或取代(即仅基于签名替换,而不考虑 Java 的可见性规则),方法就会从超类继承。此外,超类中的方法将在子类中的方法之前@BeforeEach执行。@BeforeEach-
同样,
@BeforeEach声明为接口默认方法的方法只要不被重写,就会被继承,并且默认方法将在实现该接口的类中的方法之前@BeforeEach执行 。@BeforeEach
-
-
@AfterEach只要方法未被 覆盖或取代(即仅基于签名替换,而不考虑 Java 的可见性规则),方法就会从超类继承。此外,超类中的方法将在子类中的方法之后@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()
下面的序列图有助于进一步阐明测试类执行JupiterTestEngine时实际发生的情况。DatabaseTestsDemo
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()
下面的序列图有助于进一步阐明测试类执行JupiterTestEngine时实际发生的情况。BrokenLifecycleMethodConfigDemo
|
由于上述行为,JUnit 团队建议开发人员为每个测试类或测试接口最多声明每种类型的生命周期方法(请参阅定义),除非此类生命周期方法之间不存在依赖关系。 |
6. 高级主题
6.1. JUnit 平台报告
该junit-platform-reporting工件包含TestExecutionListener生成两种类型的 XML 测试报告的实现:
传统报告和
开放测试报告。
该模块还包含TestExecutionListener可用于构建自定义报告的其他实现。有关详细信息,请参阅使用侦听器和拦截器。
|
6.1.1. 旧版 XML 格式
LegacyXmlReportGeneratingListener为 .txt 文件中的每个根生成单独的 XML 报告
TestPlan。请注意,生成的 XML 格式与基于 JUnit 4 的测试报告的事实标准兼容,该标准因 Ant 构建系统而流行。
控制台启动器
也LegacyXmlReportGeneratingListener使用它。
6.1.2. 开放测试报告 XML 格式
OpenTestReportGeneratingListener以Open Test Reporting指定的基于事件的格式编写整个执行的 XML 报告,该格式支持 JUnit 平台的所有功能,例如分层测试结构、显示名称、标签等。
侦听器是自动注册的,可以通过以下 配置参数进行配置:
junit.platform.reporting.open.xml.enabled=true|false-
启用/禁用写入报告。
junit.platform.reporting.output.dir=<path>-
配置报告的输出目录。默认情况下,
build如果找到 Gradle 构建脚本并且target找到 Maven POM,则使用;否则,使用当前工作目录。
如果启用,侦听器会
junit-platform-events-<random-id>.xml在配置的输出目录中创建一个以每个测试运行命名的 XML 报告文件。
| 开放测试报告 CLI 工具 可用于从基于事件的格式转换为更易于阅读的分层格式。 |
摇篮
对于 Gradle,可以通过系统属性启用和配置编写 Open Test Reporting 兼容的 XML 报告。以下示例将其输出目录配置为 Gradle 用于其自己的 XML 报告的同一目录。ACommandLineArgumentProvider
用于保持任务可在不同机器之间重新定位,这在使用 Gradle 的构建缓存时非常重要。
dependencies {
testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.10.1")
}
tasks.withType(Test).configureEach {
def outputDir = reports.junitXml.outputLocation
jvmArgumentProviders << ({
[
"-Djunit.platform.reporting.open.xml.enabled=true",
"-Djunit.platform.reporting.output.dir=${outputDir.get().asFile.absolutePath}"
]
} as CommandLineArgumentProvider)
}
dependencies {
testRuntimeOnly("org.junit.platform:junit-platform-reporting:1.10.1")
}
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 Surefire/Failsafe,您可以启用开放测试报告输出并将生成的 XML 文件配置为写入 Surefire/Failsafe 用于其自己的 XML 报告的同一目录,如下所示:
<project>
<!-- ... -->
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-reporting</artifactId>
<version>1.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<properties>
<configurationParameters>
junit.platform.reporting.open.xml.enabled = true
junit.platform.reporting.output.dir = target/surefire-reports
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
<!-- ... -->
</project>
控制台启动器
使用控制台启动器时,您可以通过以下方式设置配置参数来启用开放测试报告输出--config:
$ java -jar junit-platform-console-standalone-1.10.1.jar <OPTIONS> \
--config=junit.platform.reporting.open.xml.enabled=true \
--config=junit.platform.reporting.output.dir=reports
6.2. JUnit 平台套件引擎
JUnit 平台支持使用 JUnit 平台的任何测试引擎对测试套件进行声明式定义和执行。
6.2.1. 设置
除了junit-platform-suite-api和junit-platform-suite-engine工件之外,您还需要至少一个其他测试引擎及其对类路径的依赖项。有关组 ID、工件 ID 和版本的详细信息,请参阅
依赖项元数据。
6.2.2. @Suite示例
通过用它注释一个类,@Suite将其标记为 JUnit 平台上的测试套件。如以下示例所示,选择器和过滤器注释可用于控制套件的内容。
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.junit.platform.suite.api.SuiteDisplayName;
@Suite
@SuiteDisplayName("JUnit Platform Suite Demo")
@SelectPackages("example")
@IncludeClassNamePatterns(".*Tests")
class SuiteDemo {
}
|
附加配置选项
有许多配置选项可用于发现和过滤测试套件中的测试。请参阅包的 Javadocorg.junit.platform.suite.api以获取支持的注释的完整列表和更多详细信息。
|
6.3. JUnit 平台测试套件
该junit-platform-testkit工件支持在 JUnit 平台上执行测试计划,然后验证预期结果。从 JUnit Platform 1.4 开始,这种支持仅限于单个的执行TestEngine(请参阅引擎测试套件)。
6.3.1. 发动机测试套件
该org.junit.platform.testkit.engine包支持在 JUnit 平台上执行TestPlan给定的运行,然后通过流畅的 API 访问结果以验证预期结果。TestEngine该 API 的关键入口点是
EngineTestKit提供名为engine()和 的静态工厂方法execute()。建议您选择其中一种engine()变体,以便从用于构建LauncherDiscoveryRequest.
如果您更喜欢使用APILauncherDiscoveryRequestBuilder来Launcher构建您的,则必须使用中的变体
LauncherDiscoveryRequest之一。
execute()EngineTestKit |
后续示例中将使用以下使用 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。TestEngine或者,您可以通过向静态工厂方法提供一个实例来
测试您自己的方法EngineTestKit.engine(TestEngine)。
6.3.2. 断言统计数据
测试套件最常见的功能之一是能够针对TestPlan. 以下测试演示了如何在 JUnit Jupiter 中断言容器和测试TestEngine的统计信息。有关可用统计信息的详细信息,请参阅 的 Javadoc EventStatistics。
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()的计数是因为和
类都被视为容器。
startedsucceeded2JupiterTestEngineExampleTestCase |
6.3.3. 断言事件
skippedTest()例如,如果您想验证中的方法
被跳过的原因ExampleTestCase,您可以按如下方式执行。
|
有关哪些条件可用于针对事件的 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 | 选择测试类skippedTest()中的方法ExampleTestCase。 |
| 3 | 执行TestPlan. |
| 4 | 按测试事件过滤。 |
| 5 | 将测试 保存Events到局部变量。 |
| 6 | 可以选择断言预期的统计数据。 |
| 7 | 断言记录的测试事件恰好包含一个名为 的被跳过的
测试skippedTest作为"for demonstration purposes"原因。 |
failingTest()如果您想验证中的方法
抛出的异常类型ExampleTestCase,可以按如下方式执行。
|
有关哪些条件可用于针对事件和执行结果的 AssertJ 断言的详细信息,请分别查阅 |
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
class EngineTestKitFailedMethodDemo {
@Test
void verifyJupiterMethodFailed() {
EngineTestKit.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.testEvents() (4)
.assertThatEvents().haveExactly(1, (5)
event(test("failingTest"),
finishedWithFailure(
instanceOf(ArithmeticException.class), message("/ by zero"))));
}
}
| 1 | 选择 JUnit Jupiter TestEngine。 |
| 2 | 选择ExampleTestCase测试类别。 |
| 3 | 执行TestPlan. |
| 4 | 按测试事件过滤。 |
| 5 | 断言记录的测试事件恰好包含一个失败的测试,该测试的名称
failingTest为类型为异常ArithmeticException且错误消息等于"/ by zero"。 |
虽然通常是不必要的,但有时您需要验证TestPlan. 下面的测试演示了如何通过APIassertEventsMatchExactly()中的方法来实现这一点EngineTestKit。
|
由于 |
如果您想在有或没有排序要求的情况下进行部分匹配,您可以分别使用方法和。assertEventsMatchLooselyInOrder()assertEventsMatchLoosely()
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.container;
import static org.junit.platform.testkit.engine.EventConditions.engine;
import static org.junit.platform.testkit.engine.EventConditions.event;
import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
import static org.junit.platform.testkit.engine.EventConditions.started;
import static org.junit.platform.testkit.engine.EventConditions.test;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
import java.io.StringWriter;
import java.io.Writer;
import example.ExampleTestCase;
import org.junit.jupiter.api.Test;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.opentest4j.TestAbortedException;
class EngineTestKitAllEventsDemo {
@Test
void verifyAllJupiterEvents() {
Writer writer = // create a java.io.Writer for debug output
EngineTestKit.engine("junit-jupiter") (1)
.selectors(selectClass(ExampleTestCase.class)) (2)
.execute() (3)
.allEvents() (4)
.debug(writer) (5)
.assertEventsMatchExactly( (6)
event(engine(), started()),
event(container(ExampleTestCase.class), started()),
event(test("skippedTest"), skippedWithReason("for demonstration purposes")),
event(test("succeedingTest"), started()),
event(test("succeedingTest"), finishedSuccessfully()),
event(test("abortedTest"), started()),
event(test("abortedTest"),
abortedWithReason(instanceOf(TestAbortedException.class),
message(m -> m.contains("abc does not contain Z")))),
event(test("failingTest"), started()),
event(test("failingTest"), finishedWithFailure(
instanceOf(ArithmeticException.class), message("/ by zero"))),
event(container(ExampleTestCase.class), finishedSuccessfully()),
event(engine(), finishedSuccessfully()));
}
}
| 1 | 选择 JUnit 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 平台启动器 API
JUnit 5 的突出目标之一是使 JUnit 与其编程客户端(构建工具和 IDE)之间的接口更加强大和稳定。目的是将发现和执行测试的内部结构与外部所需的所有过滤和配置分离。
LauncherJUnit 5 引入了可用于发现、过滤和执行测试的概念。此外,第三方测试库(例如 Spock、Cucumber 和 FitNesse)可以通过提供自定义
TestEngine插入 JUnit 平台的启动基础架构中。
启动器 API 位于junit-platform-launcher模块中。
ConsoleLauncher项目中的
启动器 API 的一个示例使用者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
,可用于调用特定的测试或一组测试。
客户可以通过注册一个或多个LauncherDiscoveryListener实现来
LauncherDiscoveryRequestBuilder深入了解测试发现期间发生的事件。默认情况下,构建器会注册一个“失败时中止”侦听器,该侦听器会在遇到第一次发现失败后中止测试发现。可以通过配置参数LauncherDiscoveryListener更改
默认值
。junit.platform.discovery.listener.default
6.4.2. 执行测试
要执行测试,客户端可以使用LauncherDiscoveryRequest与发现阶段相同的测试或创建新请求。可以通过使用以下示例中的注册一个或多个TestExecutionListener实现来实现测试进度和报告。Launcher
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(),但您可以使用 a
TestExecutionListener来聚合结果。例如,请参见
SummaryGeneratingListener、LegacyXmlReportGeneratingListener和
UniqueIdTrackingListener。
所有TestExecutionListener方法均按顺序调用。开始事件的方法按注册顺序调用,而完成事件的方法按相反顺序调用。executionStarted在所有调用返回
之前,测试用例执行不会开始。 |
6.4.4. 注册 PostDiscoveryFilter
除了指定发现后过滤器作为LauncherDiscoveryRequest
传递给LauncherAPI 的一部分之外,PostDiscoveryFilter还将在运行时通过 Java 机制发现实现ServiceLoader,并自动应用
Launcher到那些属于请求的过滤器中。
例如,在文件中example.CustomTagFilter实现PostDiscoveryFilter和声明的类/META-INF/services/org.junit.platform.launcher.PostDiscoveryFilter
会自动加载和应用。
6.4.5。注册 LauncherSessionListener
LauncherSessionListener当 a 打开(在第一个发现并执行测试之前)和关闭(当不再发现或执行测试时)时,已注册的实现会
收到LauncherSession通知Launcher。它们可以通过LauncherConfig传递给 的来以编程方式注册LauncherFactory,或者可以在运行时通过 Java 的机制发现它们ServiceLoader并自动注册LauncherSession(除非自动注册被禁用。)
工具支持
已知以下构建工具和 IDE 可以提供全面支持LauncherSession:
-
Gradle 4.6 及更高版本
-
Maven Surefire/Failsafe 3.0.0-M6 及更高版本
-
IntelliJ IDEA 2017.3 及更高版本
其他工具也可能有效,但尚未经过明确测试。
用法示例
ALauncherSessionListener非常适合实现每个 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和方法的调用,客户端可以通过 Java
机制通过另外将
配置参数设置为 来注册自定义实现。executeLauncherInterceptorServiceLoaderjunit.platform.launcher.interceptors.enabled true
一个典型的用例是创建一个自定义的替换ClassLoaderJUnit 平台用来加载测试类和引擎实现的方法。
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或通过 API 以编程方式注册它们之外Launcher,
LauncherDiscoveryListener还可以在运行时通过 Java
ServiceLoader机制发现自定义实现,并Launcher通过LauncherFactory.
例如,在文件中example.CustomLauncherDiscoveryListener实现
LauncherDiscoveryListener和声明的
类/META-INF/services/org.junit.platform.launcher.LauncherDiscoveryListener会自动加载和注册。
6.4.8。注册一个 TestExecutionListener
除了用于Launcher以编程方式注册测试执行侦听器的公共 API 方法之外,TestExecutionListener还将在运行时通过 JavaServiceLoader机制发现自定义实现,并
Launcher通过LauncherFactory.
例如,在文件中example.CustomTestExecutionListener实现
TestExecutionListener和声明的
类/META-INF/services/org.junit.platform.launcher.TestExecutionListener会自动加载和注册。
6.4.9。配置 TestExecutionListener
当 aTestExecutionListener通过 API 以编程方式注册时Launcher,侦听器可以提供对其进行配置的编程方式 - 例如,通过其构造函数、setter 方法等。但是,当 a 通过TestExecutionListenerJavaServiceLoader机制自动注册时(请参阅
注册 TestExecutionListener),用户无法直接配置监听器。在这种情况下,a 的作者可以选择通过配置参数TestExecutionListener来配置侦听器。然后,侦听器可以通过提供的
和回调方法访问配置参数。请参阅的示例。TestPlantestPlanExecutionStarted(TestPlan)testPlanExecutionFinished(TestPlan)UniqueIdTrackingListener
6.4.10. 停用 TestExecutionListener
有时,在某些执行侦听器不处于活动状态的情况下运行测试套件可能会很有用。例如,您可能有自定义的 a TestExecutionListener,将测试结果发送到外部系统以用于报告目的,而在调试时您可能不希望报告这些调试结果。为此,请为
junit.platform.execution.listeners.deactivate 配置参数提供一个模式,以指定应为当前测试运行停用(即不注册)哪些执行侦听器。
|
只有通过文件 |
模式匹配语法
有关详细信息,请参阅模式匹配语法。
6.4.11. 配置启动器
如果您需要对测试引擎和侦听器的自动检测和注册进行细粒度控制,您可以创建一个实例并将LauncherConfig其提供给LauncherFactory. 通常, 的实例LauncherConfig是通过内置的 Fluent构建器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. 试运行模式
通过LauncherAPI 运行测试时,您可以通过将配置参数设置
为 来启用试运行模式。在此模式下,实际上不会执行任何测试,但会通知注册实例,就好像所有测试已被跳过并且其容器已成功一样。这对于测试构建配置中的更改或验证侦听器是否按预期调用而无需等待所有测试执行非常有用。junit.platform.execution.dryRun.enabled trueLauncherTestExecutionListener
6.5. 测试引擎
ATestEngine有助于发现和执行特定编程模型的测试。
6.5.1. JUnit 测试引擎
JUnit 提供了三种TestEngine实现。
-
junit-jupiter-engine:JUnit Jupiter 的核心。 -
junit-vintage-engine:JUnit 4 之上的薄层,允许使用 JUnit 平台启动器基础设施运行老式 测试(基于 JUnit 3.8 和 JUnit 4)。 -
junit-platform-suite-engine:使用 JUnit 平台启动器基础设施执行声明性测试套件。
6.5.2. 定制测试引擎
您可以通过实现junit-platform-engineTestEngine模块中的接口
并注册您的引擎来贡献您自己的自定义。
每个都TestEngine必须提供自己唯一的 ID,从 中
发现EngineDiscoveryRequest测试,并根据执行ExecutionRequest这些测试。
|
唯一的 ID前缀
junit-是为 JUnit 团队的 TestEngine 保留的JUnit 平台
|
为了在启动 JUnit 平台之前促进 IDE 和工具中的测试发现,TestEngine鼓励实现使用注释@Testable
。例如, JUnit Jupiter 中的@Test和@TestFactory注释使用 进行元注释@Testable。@Testable有关更多详细信息,请参阅 Javadoc 。
如果您的自定义TestEngine需要配置,请考虑允许用户通过配置参数提供配置。但请注意,强烈建议您对测试引擎支持的所有配置参数使用唯一的前缀。这样做将确保您的配置参数名称与其他测试引擎的配置参数名称之间不存在冲突。此外,由于配置参数可以作为 JVM 系统属性提供,因此明智的做法是避免与其他系统属性的名称发生冲突。例如,JUnit Jupiter 使用junit.jupiter.作为其所有支持的配置参数的前缀。此外,与上面有关IDjunit-前缀的
警告一样TestEngine,您不应将junit.其用作您自己的配置参数名称的前缀。
虽然目前还没有关于如何实现自定义的官方指南,但您可以参考JUnit 测试引擎TestEngine的实现或JUnit 5 wiki中列出的第三方测试引擎的实现
。您还可以在 Internet 上找到各种教程和博客,演示如何编写自定义.TestEngine
HierarchicalTestEngine是 SPI 的一个方便的抽象基础实现
TestEngine(由junit-jupiter-engine),只需要实现者提供测试发现的逻辑。TestDescriptors它实现了接口的执行Node,包括对并行执行的支持。
|
6.5.3. 注册测试引擎
TestEngine通过Java 的机制支持注册ServiceLoader。
例如,junit-jupiter-engine模块将其注册
在 JAR文件夹
中org.junit.jupiter.engine.JupiterTestEngine命名的文件中
。org.junit.platform.engine.TestEngine/META-INF/servicesjunit-jupiter-engine
6.5.4. 要求
| 本节中的“必须”、“不得”、“必需”、“应”、“不应”、“应该”、“不应该”、“建议”、“可以”和“可选”等词语是为了按照 RFC 2119 中的描述进行解释。 |
强制性要求
为了与构建工具和 IDE 进行互操作,TestEngine实施必须遵守以下要求:
-
TestDescriptor返回的值必须TestEngine.discover()是实例树的根TestDescriptor。这意味着节点及其后代之间不能有任何循环。 -
A
TestEngine必须能够发现UniqueIdSelectors它之前生成并从 . 返回的任何唯一 IDTestEngine.discover()。这使得能够选择要执行或重新运行的测试子集。 -
传递给的
executionSkipped、executionStarted和executionFinished方法 必须为 树中返回的每个节点最多调用一次。父节点必须报告为在其子节点之前开始,并在其子节点之后完成。如果节点被报告为已跳过,则不得为其后代报告任何事件。EngineExecutionListenerTestEngine.execute()TestDescriptorTestEngine.discover()
增强兼容性
遵守以下要求是可选的,但建议遵守以下要求,以增强与构建工具和 IDE 的兼容性:
-
除非指示空的发现结果,否则
TestDescriptor返回的结果TestEngine.discover()应该有子项,而不是完全动态的。这允许工具显示测试的结构并选择要执行的测试子集。 -
解析 时
UniqueIdSelectors, aTestEngine应仅返回TestDescriptor具有匹配的唯一 ID 的实例(包括其祖先),但可能会返回执行所选测试所需的其他同级节点或其他节点。 -
TestEngines应支持标记测试和容器,以便在发现测试时可以应用标记过滤器。
7. API 演变
JUnit 5 的主要目标之一是提高维护人员发展 JUnit 的能力,尽管它已在许多项目中使用。在 JUnit 4 中,许多最初作为内部构造添加的东西只能由外部扩展编写者和工具构建者使用。这使得更改 JUnit 4 特别困难,有时甚至是不可能的。
这就是 JUnit 5 为所有公开可用的接口、类和方法引入定义的生命周期的原因。
7.1. API版本和状态
每个发布的工件都有一个版本号,并且所有公开可用的接口、类和方法都使用@API Guardian项目中的
@API<major>.<minor>.<patch>进行注释。可以为注释的属性分配以下值之一。status
| 地位 | 描述 |
|---|---|
|
除 JUnit 本身之外,不得由任何代码使用。可能会被删除,恕不另行通知。 |
|
不应再使用;可能会在下一个小版本中消失。 |
|
适用于新的实验性功能,我们正在寻求反馈。 |
|
适用于至少在当前主要版本的下一个次要版本中不会以向后不兼容方式更改的功能。如果计划删除,它将被降级为 |
|
适用于在当前主要版本 ( |
如果@API注释出现在类型上,则它也被认为适用于该类型的所有公共成员。允许成员声明不同status
的较低稳定性值。
7.2. 实验性 API
下表列出了当前通过
指定为实验性的@API(status = EXPERIMENTAL)API 。依赖此类 API 时应谨慎。
| 包裹名字 | 类型名称 | 自从 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7.3. 已弃用的 API
下表列出了当前通过
指定为已弃用的@API(status = DEPRECATED)API 。您应该尽可能避免使用已弃用的 API,因为此类 API 可能会在即将发布的版本中被删除。
| 包裹名字 | 类型名称 | 自从 |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8. 贡献者
直接在 GitHub 上浏览当前的贡献者列表。
9. 发行说明
发行说明可在此处获取。
10. 附录
10.1. 可重复的构建
从版本 5.7 开始,JUnit 5 的目标是使其非 javadoc JAR 具有可 重现性。
在相同的构建条件下(例如 Java 版本),重复构建应提供相同的逐字节输出。
这意味着任何人都可以在 Maven Central/Sonatype 上重现工件的构建条件,并在本地生成相同的输出工件,从而确认存储库中的工件实际上是从该源代码生成的。
10.2. 依赖元数据
最终版本和里程碑的工件部署到Maven Central ,快照工件部署到/org/junit下的 Sonatype快照存储库。
10.2.1. JUnit平台
-
群组ID:
org.junit.platform -
版本:
1.10.1 -
工件 ID:
junit-platform-commons-
JUnit 平台的通用 API 和支持实用程序。任何带有注释的 API
@API(status = INTERNAL)仅供在 JUnit 框架本身内使用。不支持外部方对内部 API 的任何使用! junit-platform-console-
支持从控制台在 JUnit 平台上发现和执行测试。有关详细信息,请参阅控制台启动器。
junit-platform-console-standalone-
Maven Central 中的junit-platform-console-standalone目录下提供了包含所有依赖项的可执行 JAR 。有关详细信息,请参阅控制台启动器。
junit-platform-engine-
用于测试引擎的公共 API。有关详细信息,请参阅注册测试引擎。
junit-platform-jfr-
为 JUnit 平台上的 Java Flight Recorder 事件 提供
LauncherDiscoveryListener和。TestExecutionListener有关详细信息,请参阅飞行记录器支持 。 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 Platform Suite 工件,可在 Gradle 和 Maven 等构建工具中 传递依赖项
junit-platform-suite-api并简化依赖项管理。junit-platform-suite-engine junit-platform-suite-api-
用于在 JUnit 平台上配置测试套件的注释。由 JUnit Platform Suite Engine和 JUnitPlatform runner支持。
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.10.1 -
工件 ID:
junit-jupiter-
JUnit Jupiter 聚合器工件可传递性地引入对
junit-jupiter-api、junit-jupiter-params和 的依赖关系,junit-jupiter-engine以简化 Gradle 和 Maven 等构建工具中的依赖关系管理。 junit-jupiter-apijunit-jupiter-engine-
JUnit Jupiter 测试引擎实现;仅在运行时需要。
junit-jupiter-params-
支持JUnit Jupiter 中的参数化测试。
junit-jupiter-migrationsupport-
支持从 JUnit 4 迁移到 JUnit Jupiter;仅需要支持 JUnit 4 的
@Ignore注释和运行选定的 JUnit 4 规则。
10.2.3。JUnit 复古
-
群组ID:
org.junit.vintage -
版本:
5.10.1 -
工件 ID:
junit-vintage-engine-
JUnit Vintage 测试引擎实现,允许在 JUnit 平台上运行老式JUnit 测试。老式测试包括使用 JUnit 3 或 JUnit 4 API 编写的测试或使用基于这些 API 构建的测试框架编写的测试。
10.2.4。物料清单 (BOM)
在使用Maven 或Gradle引用多个上述工件时,以下 Maven 坐标下提供的物料清单POM可用于简化依赖关系管理 。
-
群组ID:
org.junit -
工件 ID:
junit-bom -
版本:
5.10.1