Android 测试框架 —— JUnit5
Junit 5 的官网文档:https://junit.org/junit5/ 或者下载 PDF 文档。
介绍
与之前的 JUnit 4 不同,JUnit 5 由三个不同的子项目的多个模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform 作为在 JVM 上启动测试框架的基础,定义了 TestEngine API,用于开发可在该平台上运行的测试框架。 此外,平台还提供了一个控制台启动器,可以通过命令行启动平台,并提供了 JUnit 平台套件引擎, 用于在平台上使用一个或多个测试引擎运行自定义测试套件。 JUnit Platform 在流行的集成开发环境(如 IntelliJ IDEA、Eclipse、NetBeans 和 Visual Studio Code)以及构建工具(如 Gradle、Maven 和 Ant)中也得到了支持。
JUnit Jupiter 是编写 JUnit 5 测试和扩展的编程模型与扩展模型的结合。Jupiter 用于在平台上运行基于 Jupiter 的测试。 JUnit Vintage 用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试,但是要求引用依赖上必须包含 JUnit 4.12 或更高版本。
- JUnit 平台 (JUnit Platform):这是 JUnit 5 的基础,负责启动测试并提供支持其他测试引擎(如 Jupiter 和 Vintage)的框架。JUnit 平台还提供了一个控制台启动器,可以通过命令行启动平台,并提供了 JUnit 平台套件引擎,用于在平台上使用一个或多个测试引擎运行自定义测试套件;
- JUnit Jupiter:这是 JUnit 5 的编程模型和扩展模型,是编写和执行 JUnit 5 测试的核心;
- JUnit Vintage:这个模块允许 JUnit 4 和 JUnit 3 的测试在 JUnit 5 平台上运行,因此 JUnit 4 的测试可以无缝迁移到 JUnit 5 环境中。
三者的关系可以总结如下:

Note
开发者可以按自己的节奏逐步迁移,JUnit 团队仍然会继续为 JUnit 4 提供维护和修复。
相比与 JUnit 4 的变化
很多 JUnit 4 的特性在 JUnit 5 上已经不再支持,但是可以通过 JUnit Vintage 框架提供支持。由于所有特定于 JUnit Jupiter 的类和注解都位于 org.junit.jupiter 基础包中,因此将 JUnit 4 和 JUnit Jupiter 同时放在类路径中不会导致任何冲突。因此,可以安全地将现有的 JUnit 4 测试与 JUnit Jupiter 测试共存。在迁移 JUnit 4 到 JUnit 5 过程中以下这些应该注意:
- 注解位于
org.junit.jupiter.api
包中。 - 断言方法位于
org.junit.jupiter.api.Assertions
。 注意,你仍然可以继续使用org.junit.Assert
中的断言方法,或者使用其他断言库,如 AssertJ、Hamcrest、Truth 等。 - 假设方法位于
org.junit.jupiter.api.Assumptions
。 注意,JUnit Jupiter 5.4 及后续版本支持 JUnit 4 中org.junit.Assume
类的方法进行假设操作。具体来说,JUnit Jupiter 支持 JUnit 4 中的 AssumptionViolatedException,用来表示测试应被中止,而不是标记为失败。 @Before
和@After
不再存在;请使用@BeforeEach
和@AfterEach
。@BeforeClass
和@AfterClass
不再存在;请使用@BeforeAll
和@AfterAll
。@Ignore
不再存在;请使用@Disabled
或其他内置的执行条件。@Category
不再存在;请使用@Tag
。@RunWith
不再存在;已被@ExtendWith
取代。@Rule
和@ClassRule
不再存在;已被@ExtendWith
和@RegisterExtension
取代。@Test(expected = …)
和ExpectedException
规则不再存在;请改用Assertions.assertThrows(…)
。- JUnit Jupiter 中的断言和假设方法将失败消息作为最后一个参数,而不是第一个参数。
下载和安装
可以从 JUnit 5 的 MVN Repository 仓库获取到所有版本的 Maven、Gradle 依赖。
Note
JUnit 5 需要 Java 8 或者更高的版本。
Maven
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
Gradle
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.11.4'
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.4'
// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.4")
Hello World
在项目中添加 JUnit 5 的相关依赖后,开发一个 Hello World 体验一下 JUnit 5 的魅力吧:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("Hello World Test")
public class HelloWorldTest {
@Test
public void test() {
System.out.println("Hello World");
}
}
通过 @DisplayName
修改用例的显示名字,点击运行按钮:
基本概念
概念 | 描述 |
---|---|
测试类 | 任何包含至少一个测试方法的顶级类、静态成员类或 @Nested 类,即容器。测试类不能是抽象类,并且必须有一个构造函数。Java 记录类也被支持。 |
测试方法 | 任何实例方法,直接使用 @Test 、@RepeatedTest 、@ParameterizedTest 、@TestFactory 或 @TestTemplate 注解或元注解。除了 @Test 外,这些注解会在测试树中创建一个容器,用于组织测试,或者(对于 @TestFactory )可能创建其他容器。 |
容器 | 测试树中的一个节点,包含其他容器或测试作为其子节点(例如,测试类)。 |
测试 | 测试树中的一个节点,当执行时验证预期行为(例如,一个 @Test 方法)。 |
生命周期方法 | 任何直接使用 @BeforeAll 、@AfterAll 、@BeforeEach 或 @AfterEach 注解或元注解的方法。 |
Warning
测试方法和生命周期方法可以在当前测试类中局部声明,也可以从超类或接口继承(参见测试接口和默认方法)。
此外,测试方法和生命周期方法不得是抽象的,并且不得返回值(除了 @TestFactory
方法需要返回值)。
Warning
测试类、测试方法和生命周期方法不要求是 public
,但不能是 private
。
一般情况下,除非出于技术原因,建议省略测试类、测试方法和生命周期方法的 public
修饰符。
例如,当测试类被另一个包中的测试类继承时,可能需要使用 public
。另一个将类和方法设为 public
的技术原因是,在使用 Java 模块系统时,简化模块路径上的测试。
对于一个标准的 JUnit 5 的测试类来说,可以参考如下的写法:
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() {
}
}
注解
JUnit Jupiter 支持以下注解,用于配置测试和扩展框架功能。除非另有说明,所有核心注解都位于 junit-jupiter-api
模块的
org.junit.jupiter.api
包中。
注解 | 描述 |
---|---|
@Test |
表明该方法是一个测试方法。与 JUnit 4 中的 @Test 注解不同,该注解不声明任何属性,因为 JUnit Jupiter 中的测试扩展基于各自的专用注解运行。此类方法会被继承,除非被重写。 |
@ParameterizedTest |
表明该方法是一个参数化测试。此类方法会被继承,除非被重写。 |
@RepeatedTest |
表明该方法是一个重复测试的测试模板。此类方法会被继承,除非被重写。 |
@TestFactory |
表明该方法是一个动态测试的测试工厂。此类方法会被继承,除非被重写。 |
@TestTemplate |
表明该方法是一个测试用例模板,设计为根据注册的提供者返回的调用上下文数量多次调用。此类方法会被继承,除非被重写。 |
@TestClassOrder |
用于配置注解的测试类中 @Nested 测试类的执行顺序。此类注解会被继承。 |
@TestMethodOrder |
用于配置注解的测试类的测试方法执行顺序,类似于 JUnit 4 中的 @FixMethodOrder 。此类注解会被继承。 |
@TestInstance |
用于配置注解测试类的测试实例生命周期。此类注解会被继承。 |
@DisplayName |
声明一个自定义的显示名称用于测试类或测试方法。此类注解不会被继承。 |
@DisplayNameGeneration |
声明一个自定义的显示名称生成器用于测试类。此类注解会被继承。 |
@BeforeEach |
表示注解的方法应该在当前类中的每个 @Test 、@RepeatedTest 、@ParameterizedTest 或 @TestFactory 方法之前执行;类似于 JUnit 4 的 @Before 。这些方法是可以继承的,除非被重写。 |
@AfterEach |
表示注解的方法应该在当前类中的每个 @Test 、@RepeatedTest 、@ParameterizedTest 或 @TestFactory 方法之后执行;类似于 JUnit 4 的 @After 。这些方法是可以继承的,除非被重写。 |
@BeforeAll |
表示注解的方法应该在当前类中的所有 @Test 、@RepeatedTest 、@ParameterizedTest 和 @TestFactory 方法之前执行;类似于 JUnit 4 的 @BeforeClass 。这些方法是可以继承的,除非被重写,并且除非使用 "每类" 测试实例生命周期,否则必须是 static 。 |
@AfterAll |
表示注解的方法应该在当前类中的所有 @Test 、@RepeatedTest 、@ParameterizedTest 和 @TestFactory 方法之后执行;类似于 JUnit 4 的 @AfterClass 。这些方法是可以继承的,除非被重写,并且除非使用 "每类" 测试实例生命周期,否则必须是 static 。 |
@Nested |
表示注解的类是一个非静态的嵌套测试类。在 Java 8 至 Java 15 中,除非使用 "每类" 测试实例生命周期,否则不能直接在 @Nested 测试类中使用 @BeforeAll 和 @AfterAll 方法。从 Java 16 开始,可以在 @Nested 测试类中将 @BeforeAll 和 @AfterAll 方法声明为 static ,无论使用何种测试实例生命周期模式。这些注解不会被继承。 |
@Tag |
用于声明标签以便于过滤测试,可以在类或方法级别使用;类似于 TestNG 中的测试组或 JUnit 4 中的类别。这些注解在类级别是可以继承的,但在方法级别不会继承。 |
@Disabled |
用于禁用测试类或测试方法;类似于 JUnit 4 的 @Ignore 。这些注解不会被继承。 |
@AutoClose |
表示注解的字段代表一个资源,该资源将在测试执行后自动关闭。 |
@Timeout |
用于在测试、测试工厂、测试模板或生命周期方法的执行超过给定持续时间时使其失败。这些注解是可以继承的。 |
@TempDir |
用于通过字段注入或参数注入在生命周期方法或测试方法中提供临时目录;位于 org.junit.jupiter.api.io 包中。这些字段是可以继承的。 |
@ExtendWith |
用于声明性地注册扩展。 |
@RegisterExtension |
用于通过字段编程式地注册扩展。这些字段是可以继承的。 |
元注解和组合注解
JUnit Jupiter 注解可以作为元注解使用。通过自定义组合注解,将元注解重新构造成一个新的注解。自定义后的注解将自动继承其元注解的语义。
例如,代替在代码中复制粘贴 @Tag("fast")
,你可以创建一个名为 @Fast
的自定义组合注解。然后,@Fast
可以作为
@Tag("fast")
的替代品直接使用:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}
以下的 @Test
方法展示了如何使用 @Fast
注解:
@Fast
@Test
void myFastTest() {
// ...
}
甚至可以进一步扩展,通过引入一个自定义的 @FastTest
注解,它可以作为 @Tag("fast")
和 @Test
的替代品使用。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}
JUnit 会自动识别以下内容作为一个标记为“fast”的 @Test
方法:
@FastTest
void myFastTest() {
// ...
}
@DisplayName
测试类和测试方法可以通过 @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() {
}
}
Note
JUnit Jupiter 支持通过 @DisplayNameGeneration
注解配置自定义显示名称生成器。
通过 @DisplayName
注解提供的值始终优先于 DisplayNameGenerator
生成的显示名称。
详见 JUnit 5 官网《2.4.1. Display Name Generators》。
@Disabled
可以通过 @Disabled
注解、条件测试执行中讨论的注解之一,或者自定义的
ExecutionCondition
来禁用整个测试类或单个测试方法。
当 @Disabled
注解应用于类级别时,该类中的所有测试方法都会被自动禁用。
如果一个测试方法通过 @Disabled
注解被禁用,这将阻止该测试方法的执行,以及方法级别的生命周期回调(如 @BeforeEach
方法、@AfterEach
方法和相应的扩展 API)。然而,这并不会阻止测试类的实例化,也不会阻止类级别的生命周期回调(如 @BeforeAll
方法、@AfterAll
方法和相应的扩展 API)的执行。
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
或者:
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() {
}
}
@(En|Dis)abledOnOs
可以通过 @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() {
// ...
}
@En(Dis)abledOnJre 和 @En(Dis)abledForJreRange
可以通过 @EnabledOnJre
和 @DisabledOnJre
注解,在特定版本的 Java 运行时环境(JRE)上启用或禁用容器或测试,或者通过
@EnabledForJreRange
和 @DisabledForJreRange
注解,在特定版本范围的 JRE 上启用或禁用。该范围默认以 JRE.JAVA_8
作为下限(最小值),JRE.OTHER 作为上限(最大值),允许使用半开区间。
@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() {
// ...
}
@En(Dis)abledIfSystemProperty
可以通过 @EnabledIfSystemProperty
和 @DisabledIfSystemProperty
注解,根据指定的 JVM 系统属性的值来启用或禁用容器或测试。通过
matches
属性提供的值将被解释为正则表达式。
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
// ...
}
@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
// ...
}
@En(Dis)abledIfEnvironmentVariable
可以通过 @EnabledIfEnvironmentVariable
和 @DisabledIfEnvironmentVariable
注解,根据底层操作系统中指定的环境变量的值来启用或禁用容器或测试。通过
matches
属性提供的值将被解释为正则表达式。
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
// ...
}
@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
// ...
}
@EnabledIf
作为实现 ExecutionCondition
的替代方案,可以通过 @EnabledIf
和 @DisabledIf
注解,根据条件方法来启用或禁用容器或测试。条件方法必须返回布尔值,并且可以不接受任何参数或只接受一个 ExtensionContext
参数。
以下测试类演示了如何通过 @EnabledIf
和 @DisabledIf
配置一个名为 customCondition
的本地方法:
@Test
@EnabledIf("customCondition")
void enabled() {
// ...
}
@Test
@DisabledIf("customCondition")
void disabled() {
// ...
}
boolean customCondition() {
return true;
}
另外,条件方法也可以位于测试类之外。在这种情况下,必须通过其全限定名进行引用,正如以下示例所示:
package example;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
class ExternalCustomConditionDemo {
@Test
@EnabledIf("example.ExternalCondition#customCondition")
void enabled() {
// ...
}
}
class ExternalCondition {
static boolean customCondition() {
return true;
}
}
Warning
在以下几种情况下,条件方法需要是静态的:
- 当 @EnabledIf 或 @DisabledIf 被用在类级别时
- 当 @EnabledIf 或 @DisabledIf 被用在 @ParameterizedTest 或 @TestTemplate 方法上时
- 当条件方法位于外部类中时
在其他情况下,可以使用静态方法或实例方法作为条件方法。
@Tag
测试类和方法可以通过 @Tag
注解进行标记。标记了 @Tag
注解的类和方法,可以在执行测试时选择特定的用例进行执行。
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
@Tag("model")
class TaggingDemo {
@Test
@Tag("taxes")
void testingTaxCalculation() {
}
}
@Nested
@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 中的测试执行树将在图形用户界面中呈现类似以下的结构:

@RepeatedTest
JUnit Jupiter 提供了通过在方法上添加 @RepeatedTest
注解并指定希望的重复次数来重复执行测试的功能。每次重复执行的测试都像常规的
@Test 方法一样执行,并完全支持相同的生命周期回调和扩展。
以下示例演示了如何声明一个名为 repeatedTest()
的测试方法,该方法将被自动重复执行 10 次:
@RepeatedTest(10)
void repeatedTest() {
// ...
}
@ParameterizedTest
@ParameterizedTest
标记方法时表明该方法是一个参数化测试。参数化测试使得能够使用不同的参数多次运行同一个测试。它们的声明方式与常规的
@Test
方法相同。@ParameterizedTest
的方法必须声明至少一个数据源,用于提供每次调用的参数,然后在测试方法中使用这些参数。
以下示例演示了一个参数化测试,它使用 @ValueSource
注解指定一个字符串数组作为参数源:
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@TestTemplate
TestInfo
在所有之前的 JUnit 版本中,测试构造函数或方法不允许带有参数(至少在标准的 Runner 实现中是如此)。作为 JUnit Jupiter 的主要变化之一,现在测试构造函数和方法都允许带有参数。这为测试提供了更大的灵活性,并支持构造函数和方法的依赖注入。
ParameterResolver
定义了一个 API,用于那些希望在运行时动态解析参数的测试扩展。如果测试类的构造函数、
测试方法或生命周期方法接受参数,则该参数必须通过注册的 ParameterResolver
在运行时进行解析。
目前有三个内置的解析器,它们会自动注册:TestInfoParameterResolver
、RepetitionExtension
和
TestReporterParameterResolver
。
TestInfoParameterResolver
TestInfoParameterResolver
:如果构造函数或方法的参数类型是 TestInfo
,TestInfoParameterResolver
将提供一个与当前容器或测试对应的
TestInfo
实例,作为该参数的值。然后,可以使用 TestInfo
来检索有关当前容器或测试的信息,例如显示名称、测试类、测试方法和关联的标签。显示名称可以是技术名称,如测试类或测试方法的名称,或者是通过
@DisplayName
配置的自定义名称。
TestInfo
作为 JUnit 4 中 TestName
规则的替代方案。以下示例演示了如何将 TestInfo
注入到测试构造函数、@BeforeEach
方法和
@Test
方法中。
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
@DisplayName("TestInfo Demo")
class TestInfoDemo {
TestInfoDemo(TestInfo testInfo) {
assertEquals("TestInfo Demo", testInfo.getDisplayName());
}
@BeforeEach
void init(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}
@Test
@DisplayName("TEST 1")
@Tag("my-tag")
void test1(TestInfo testInfo) {
assertEquals("TEST 1", testInfo.getDisplayName());
assertTrue(testInfo.getTags().contains("my-tag"));
}
@Test
void test2() {
}
}
RepetitionExtension
如果 @RepeatedTest
、@BeforeEach
或 @AfterEach
方法的参数类型是 RepetitionInfo
,RepetitionExtension
会提供一个
RepetitionInfo
实例。然后,可以使用 RepetitionInfo
来检索当前重复次数的信息、总重复次数、失败的重复次数以及对应
@RepeatedTest
的失败阈值。不过需要注意的是,RepetitionExtension
只在 @RepeatedTest
上下文中注册。
TestReporterParameterResolver
如果构造函数或方法的参数类型是 TestReporter
,TestReporterParameterResolver
会提供一个 TestReporter
实例。
TestReporter
可用于发布当前测试运行的附加数据。这些数据可以通过 TestExecutionListener
中的 reportingEntryPublished()
方法进行消费,从而可以在
IDE 中查看或包含在报告中。
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);
}
}
断言(Assertion)
JUnit Jupiter 提供了许多与 JUnit 4 相同的断言方法,并新增了一些特别适合与 Java 8 lambda 表达式配合使用的断言方法。所有
JUnit Jupiter 的断言方法都是 org.junit.jupiter.api.Assertions
类中的静态方法。断言方法可以选择性地接受第三个参数作为断言消息,
该参数可以是 String
类型或 Supplier<String>
类型。
当使用 Supplier<String>
(例如 lambda 表达式)时,消息会延迟求值。这可以带来性能优势,特别是在消息构建复杂或耗时的情况下,
因为消息仅在断言失败时才会被评估。
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import example.domain.Person;
import example.util.Calculator;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
class AssertionsDemo {
private final Calculator calculator = new Calculator();
private final Person person = new Person("Jane", "Doe");
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
// Lazily evaluates generateFailureMessage('a','b').
assertTrue('a' < 'b', () -> generateFailureMessage('a','b'));
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and all
// failures will be reported together.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}
@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
new CountDownLatch(1).await();
});
}
private static String greeting() {
return "Hello, World!";
}
private static String generateFailureMessage(char a, char b) {
return "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily." + (a < b);
}
}
尽管 JUnit Jupiter 提供的断言功能足以应对许多测试场景,但有时可能需要更强大的功能和额外的功能,如匹配器。在这种情况下,JUnit 团队推荐使用第三方断言库,如 AssertJ、Hamcrest、Truth 等。因此,开发人员可以自由选择自己喜欢的断言库。
例如,匹配器与流式 API 的组合可以使断言更加描述性和可读。尽管如此,JUnit Jupiter 的 org.junit.jupiter.api.Assertions
类并没有像 JUnit 4 中 org.junit.Assert
类那样提供接受 Hamcrest Matcher 的 assertThat()
方法。
相反,开发人员被鼓励使用第三方断言库提供的匹配器内建支持。
以下示例演示了如何在 JUnit Jupiter 测试中使用 Hamcrest 的 assertThat()
支持。只要将 Hamcrest 库添加到类路径中,就可以静态导入
assertThat()
、is()
和 equalTo()
等方法,并像下面的 assertWithHamcrestMatcher()
方法那样在测试中使用它们。
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class HamcrestAssertionsDemo {
private final Calculator calculator = new Calculator();
@Test
void assertWithHamcrestMatcher() {
assertThat(calculator.subtract(4, 1), is(equalTo(3)));
}
}
假设(Assumptions)
假设(Assumptions)通常用于在某些测试的条件不满足时,抛出 org.opentest4j.TestAbortedException
类型的异常,从而中止测试的执行,而不是将其标记为失败。
比如,当测试依赖于系统的某个配置属性时,当这个属性当前又不存在于系统中,此时继续进行测试就会变得毫无意义。
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));
}
}
Note
也可以使用 JUnit 4 中 org.junit.Assume
类的方法来进行假设。具体来说,JUnit Jupiter 支持 JUnit 4
的 AssumptionViolatedException
异常,用于表示测试应该中止,而不是被标记为失败。
异常处理
JUnit Jupiter 提供了强大的支持来处理测试中出现的异常。在 JUnit Jupiter
中,如果测试方法、生命周期方法或扩展中抛出异常且该异常没有在方法内部被捕获,框架将把该测试或测试类标记为失败。
在以下示例中,failsDueToUncaughtException()
方法抛出一个 ArithmeticException
异常。由于该异常没有在测试方法内部被捕获,JUnit
Jupiter 将把该测试标记为失败。
private final Calculator calculator = new Calculator();
@Test
void failsDueToUncaughtException() {
// The following throws an ArithmeticException due to division by
// zero, which causes a test failure.
calculator.divide(1, 0);
}
JUnit Jupiter 提供了一些异常的断言方法,用于测试在预期条件下是否抛出了特定的异常。assertThrows()
和
assertThrowsExactly()
断言是验证代码是否通过抛出适当的异常来正确响应错误条件的关键方法。
assertThrows()
方法用于验证在执行提供的可执行代码块时是否抛出了特定类型的异常。 它不仅检查抛出异常的类型,还检查其子类,因此适用于更为广泛的异常处理测试。assertThrows()
断言方法返回抛出的异常对象,以便对其进行进一步的断言操作;assertThrowsExactly()
方法用于当需要断言抛出的异常恰好是特定类型,而不允许其是预期异常类型的子类时使用。 这在需要验证精确的异常处理行为时非常有用。与assertThrows()
类似,assertThrowsExactly()
断言方法也会返回抛出的异常对象, 以便对其进行进一步的断言操作。
以下是这两个断言方法的示例;
@Test
void testExpectedExceptionIsThrown() {
// The following assertion succeeds because the code under assertion
// throws the expected IllegalArgumentException.
// The assertion also returns the thrown exception which can be used for
// further assertions like asserting the exception message.
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("expected message");
});
assertEquals("expected message", exception.getMessage());
// The following assertion also succeeds because the code under assertion
// throws IllegalArgumentException which is a subclass of RuntimeException.
assertThrows(RuntimeException.class, () -> {
throw new IllegalArgumentException("expected message");
});
}
@Test
void testExpectedExceptionIsThrown() {
// The following assertion succeeds because the code under assertion throws
// IllegalArgumentException which is exactly equal to the expected type.
// The assertion also returns the thrown exception which can be used for
// further assertions like asserting the exception message.
IllegalArgumentException exception =
assertThrowsExactly(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("expected message");
});
assertEquals("expected message", exception.getMessage());
// The following assertion fails because the assertion expects exactly
// RuntimeException to be thrown, not subclasses of RuntimeException.
assertThrowsExactly(RuntimeException.class, () -> {
throw new IllegalArgumentException("expected message");
});
}
尽管任何从测试方法中抛出的异常都会导致测试失败,但在某些情况下,明确断言某个代码块在测试方法中不抛出异常是有益的。
assertDoesNotThrow()
断言可以在你想验证特定代码段没有抛出任何异常时使用。这种断言确保了在执行给定代码块时没有出现意外的异常。
@Test
void testExceptionIsNotThrown() {
assertDoesNotThrow(() -> {
shouldNotThrowException();
});
}
void shouldNotThrowException() {
}
测试顺序
方法的执行顺序
尽管真正的单元测试通常不应该依赖于执行顺序,但在某些情况下,确实需要强制指定测试方法的执行顺序——例如,
在编写集成测试或功能测试时,测试的顺序很重要,尤其是与 @TestInstance(Lifecycle.PER_CLASS)
配合使用时。
要控制测试方法的执行顺序,可以在测试类或测试接口上使用 @TestMethodOrder
注解,并指定所需的 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.testclass.order.default
配置参数,指定希望使用的
ClassOrderer
的完全限定类名。ClassOrderer
需要应用在所有顶层的测试类(包括静态嵌套测试类)和 @Nested
测试类。
提供的类必须实现 ClassOrderer
接口。或使用以下内置的 ClassOrderer
实现之一。
ClassOrderer.ClassName
:根据测试类的完全限定类名按字母数字顺序对测试类进行排序。ClassOrderer.DisplayName
:根据测试类的显示名称按字母数字顺序对测试类进行排序。ClassOrderer.OrderAnnotation
:根据@Order
注解中指定的值按数字顺序对测试类进行排序。ClassOrderer.Random
:伪随机地对测试类进行排序,并支持自定义种子的配置。
要为 @Nested
测试类在本地配置测试类执行顺序,可以在包含 @Nested
测试类的外部类上声明 @TestClassOrder
注解,并在
@TestClassOrder
注解中直接提供要使用的 ClassOrderer 实现的类引用。配置的 ClassOrderer
将递归应用于 @Nested
测试类及其内部的
@Nested
测试类。需要注意的是,本地的 @TestClassOrder
声明始终会覆盖通过 junit.jupiter.testclass.order.default
配置参数全局配置的 @TestClassOrder
声明或继承的 @TestClassOrder
声明。
以下示例演示了如何保证 @Nested
测试类按照通过 @Order
注解指定的顺序执行:
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class OrderedNestedTestClassesDemo {
@Nested
@Order(1)
class PrimaryTests {
@Test
void test1() {
}
}
@Nested
@Order(2)
class SecondaryTests {
@Test
void test2() {
}
}
}
测试接口和默认方法
JUnit Jupiter 允许在接口的默认方法上声明 @Test
、@RepeatedTest
、@ParameterizedTest
、@TestFactory
、@TestTemplate
、
@BeforeEach
和 @AfterEach
注解。@BeforeAll
和 @AfterAll
可以声明在测试接口的静态方法上,或者在测试接口或测试类上使用
@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 可以声明在测试接口上,这样实现该接口的类就会自动继承它的标签和扩展。
@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}
class TestInterfaceDemo implements TestLifecycleLogger,
TimeExecutionLogger, TestInterfaceDynamicTestsDemo {
@Test
void isEqualValue() {
assertEquals(1, "a".length(), "is always equal");
}
}