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.9.2</version>
    <scope>test</scope>
</dependency>

目前 Eclipse 和 IntelliJ 已直接支持在 JUnit 平台上运行单元测试。当然,我们也可以通过 Maven Test 目标运行测试。

特别说明: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 – 在每个测试方法前执行(原 @Before
  • @AfterEach – 在每个测试方法后执行(原 @After
  • @BeforeAll – 在当前类所有测试方法前执行(原 @BeforeClass
  • @AfterAll – 在当前类所有测试方法后执行(原 @AfterClass
  • @Disabled – 禁用测试类或方法(原 @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("成功");
}

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

可以看到,我们可以通过新注解修改显示名称或带注释禁用方法。

4.3. @AfterEach@AfterAll

最后讨论测试执行后的操作方法:

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

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

⚠️ 注意:使用 @AfterAll 注解的方法也必须是静态的

5. 断言与假设

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

5.1. 断言

断言已移至 org.junit.jupiter.api.Assertions 并得到显著改进。如前所述,现在可以在断言中使用 Lambda:

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

虽然这个例子很简单,但使用 Lambda 表达式作为断言消息的优势在于延迟计算,当消息构造成本较高时能节省时间和资源。

现在还可以通过 assertAll() 分组断言,它会用 MultipleFailuresError 报告组内所有失败断言:

@Test
void groupAssertions() {
    int[] numbers = {0, 1, 2, 3, 4};
    assertAll("数字数组",
        () -> 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. 测试套件

继续探索 JUnit 5 的新特性,我们介绍测试套件概念——聚合多个测试类以便一起运行。JUnit 5 提供了 @SelectPackages@SelectClasses 两个注解创建测试套件。

⚠️ 注意:目前大多数 IDE 尚不支持这些功能。

先看第一个示例:

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

@SelectPackage 用于指定运行测试套件时要选择的包名。本例中会运行所有测试。第二个注解 @SelectClasses 用于指定要选择的类:

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

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

8. 动态测试

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

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

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

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

⚠️ 注意:@TestFactory 方法不能是 private 或 static。测试数量是动态的,取决于 ArrayList 的大小。

9. 总结

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

我们探讨了 JUnit 5 架构的重大变革,包括平台启动器、IDE、其他单元测试框架、构建工具集成等。此外,JUnit 5 更深入地集成了 Java 8,特别是 Lambda 和 Stream 概念。

本文使用的示例代码可在 GitHub 项目 中找到。


原始标题:A Guide to JUnit 5

« 上一篇: Apache Camel 入门指南
» 下一篇: Spring BeanFactory 指南