JUnit 5 使用指南

更新于 2025-12-30

baeldung 2023-11-17

1. 概述

JUnit 是 Java 生态系统中最流行的单元测试框架之一。JUnit 5 版本引入了许多令人兴奋的创新,旨在支持 Java 8 及更高版本的新特性,并支持多种不同的测试风格。

2. Maven 依赖

设置 JUnit 5.x.0 非常简单,只需在 pom.xml 中添加以下依赖项:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.11.0-M2</version>
    <scope>test</scope>
</dependency>

此外,Eclipse 和 IntelliJ IDEA 现已直接支持在 JUnit Platform 上运行单元测试。当然,我们也可以通过 Maven 的 test 目标来运行测试。

IntelliJ 默认支持 JUnit 5,因此在 IntelliJ 中运行 JUnit 5 测试非常简单:只需右键点击 → “Run” 或使用快捷键 Ctrl+Shift+F10

需要注意的是,该版本要求使用 Java 8 或更高版本。

3. 架构

JUnit 5 由三个子项目组成,每个子项目包含若干模块。

3.1 JUnit Platform

平台负责在 JVM 上启动测试框架。它为 JUnit 与其客户端(如构建工具)之间定义了一个稳定且强大的接口。

该平台可以轻松地将客户端与 JUnit 集成,以发现和执行测试。

它还定义了 TestEngine API,用于开发可在 JUnit 平台上运行的测试框架。通过实现自定义的 TestEngine,我们可以将第三方测试库直接集成到 JUnit 中。

3.2 JUnit Jupiter

此模块包含用于在 JUnit 5 中编写测试的新编程模型和扩展模型。相较于 JUnit 4,新增了以下注解:

  • @TestFactory – 标识一个方法为动态测试的测试工厂
  • @DisplayName – 为测试类或测试方法定义自定义显示名称
  • @Nested – 表示被注解的类是一个嵌套的、非静态的测试类
  • @Tag – 声明用于过滤测试的标签
  • @ExtendWith – 注册自定义扩展
  • @BeforeEach – 表示该方法将在每个测试方法之前执行(原 JUnit 4 中的 @Before
  • @AfterEach – 表示该方法将在每个测试方法之后执行(原 JUnit 4 中的 @After
  • @BeforeAll – 表示该方法将在当前类中所有测试方法之前执行(原 JUnit 4 中的 @BeforeClass
  • @AfterAll – 表示该方法将在当前类中所有测试方法之后执行(原 JUnit 4 中的 @AfterClass
  • @Disabled – 禁用测试类或方法(原 JUnit 4 中的 @Ignore

3.3 JUnit Vintage

JUnit Vintage 支持在 JUnit 5 平台上运行基于 JUnit 3 和 JUnit 4 编写的测试。

4. 基础注解

为了讨论新注解,我们将本节分为三组:测试前执行、测试期间(可选)、测试后执行。

4.1 @BeforeAll@BeforeEach

以下是一个在主测试用例执行前运行的简单代码示例:

@BeforeAll
static void setup() {
    log.info("@BeforeAll - 在此类所有测试方法之前仅执行一次");
}

@BeforeEach
void init() {
    log.info("@BeforeEach - 在此类每个测试方法之前执行");
}

请注意,带有 @BeforeAll 注解的方法必须是静态的,否则代码无法编译。

4.2 @DisplayName@Disabled

接下来我们来看一些新的可选测试方法:

@DisplayName("单个测试成功")
@Test
void testSingleSuccessTest() {
    log.info("Success");
}

@Test
@Disabled("尚未实现")
void testShowSomething() {
}

如上所示,我们可以使用新注解来更改显示名称,或附带说明禁用某个方法。

4.3 @AfterEach@AfterAll

最后,我们来看测试执行后相关的操作方法:

@AfterEach
void tearDown() {
    log.info("@AfterEach - 在每个测试方法之后执行。");
}

@AfterAll
static void done() {
    log.info("@AfterAll - 在所有测试方法之后执行。");
}

请注意,带有 @AfterAll 注解的方法也必须是静态方法。

5. 断言(Assertions)与假设(Assumptions)

JUnit 5 充分利用了 Java 8 的新特性,尤其是 Lambda 表达式。

5.1 断言

断言已被移至 org.junit.jupiter.api.Assertions,并得到了显著增强。如前所述,我们现在可以在断言中使用 Lambda 表达式:

@Test
void lambdaExpressions() {
    List<Integer> numbers = Arrays.asList(1, 2, 3);
    assertTrue(numbers.stream()
      .mapToInt(Integer::intValue)
      .sum() > 5, () -> "总和应大于 5");
}

虽然上述示例较为简单,但使用 Lambda 表达式作为断言消息的优势在于其惰性求值(lazy evaluation),如果消息构造开销较大,这将节省时间和资源。

现在还可以使用 assertAll() 对多个断言进行分组。如果组内有任意断言失败,将抛出 MultipleFailuresError 并报告所有失败:

@Test
void groupAssertions() {
    int[] numbers = {0, 1, 2, 3, 4};
    assertAll("numbers",
        () -> assertEquals(numbers[0], 1),
        () -> assertEquals(numbers[3], 3),
        () -> assertEquals(numbers[4], 1)
    );
}

这意味着现在可以更安全地编写复杂的断言,因为我们可以精确定位任何失败的位置。

5.2 假设

假设用于仅在满足某些条件时才运行测试。通常用于测试所需的外部条件(这些条件与被测内容无直接关系)。

我们可以使用 assumeTrue()assumeFalse()assumingThat() 来声明假设:

@Test
void trueAssumption() {
    assumeTrue(5 > 1);
    assertEquals(5 + 2, 7);
}

@Test
void falseAssumption() {
    assumeFalse(5 < 1);
    assertEquals(5 + 2, 7);
}

@Test
void assumptionThat() {
    String someString = "Just a string";
    assumingThat(
        someString.equals("Just a string"),
        () -> assertEquals(2 + 2, 4)
    );
}

如果假设失败,将抛出 TestAbortedException,并且该测试将被跳过。

假设同样支持 Lambda 表达式。

6. 异常测试

JUnit 5 提供了两种异常测试方式,均可通过 assertThrows() 方法实现:

@Test
void shouldThrowException() {
    Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
        throw new UnsupportedOperationException("Not supported");
    });
    assertEquals("Not supported", exception.getMessage());
}

@Test
void assertThrowsException() {
    String str = null;
    assertThrows(IllegalArgumentException.class, () -> {
        Integer.valueOf(str);
    });
}

第一个示例验证了抛出异常的详细信息,第二个则验证了异常类型。

7. 测试套件(Test Suites)

继续介绍 JUnit 5 的新特性,我们将探讨如何将多个测试类聚合为一个测试套件,以便一起运行。JUnit 5 提供了两个注解:@SelectPackages@SelectClasses,用于创建测试套件。

请注意,在当前阶段,大多数 IDE 尚未完全支持这些功能。

首先看 @SelectPackages 的用法:

@Suite
@SelectPackages("com.msm")
@ExcludePackages("com.msm.suites")
public class AllUnitTest {}

@SelectPackages 用于指定运行测试套件时要选择的包名。在本例中,它将运行所有测试。而 @SelectClasses 则用于指定运行测试套件时要选择的具体测试类:

@Suite
@SelectClasses({AssertionTest.class, AssumptionTest.class, ExceptionTest.class})
public class AllUnitTest {}

例如,上述类将创建一个包含三个测试类的套件。注意,这些类不必位于同一个包中。

8. 动态测试(Dynamic Tests)

最后一个要介绍的主题是 JUnit 5 的动态测试功能,它允许我们在运行时动态生成并执行测试用例。与在编译时就固定数量的静态测试不同,动态测试允许我们在运行时定义测试用例。

动态测试可通过带有 @TestFactory 注解的工厂方法生成。请看以下代码:

@TestFactory
Stream<DynamicTest> translateDynamicTestsFromStream() {
    return in.stream()
      .map(word ->
          DynamicTest.dynamicTest("Test translate " + word, () -> {
            int id = in.indexOf(word);
            assertEquals(out.get(id), translate(word));
          })
    );
}

这个例子非常直观易懂。我们希望使用两个 ArrayList(分别名为 inout)来翻译单词。工厂方法必须返回 StreamCollectionIterableIterator。本例中我们选择了 Java 8 的 Stream

请注意,@TestFactory 方法不能是 privatestatic。测试数量是动态的,取决于 ArrayList 的大小。

9. 结论

本文简要介绍了 JUnit 5 带来的主要变化。

我们探讨了 JUnit 5 在架构上的重大改进,包括平台启动器、IDE 集成、与其他单元测试框架的兼容性,以及与构建工具的整合等。此外,JUnit 5 与 Java 8 的集成更加紧密,特别是在 Lambda 表达式和 Stream 流方面的应用。