第3章 在JUnit5中搭建基础Test Fixture:@BeforeEach、@AfterEach、@BeforeAll、@AfterAll实战

3.1 原理:Test Fixture与生命周期钩子

Test Fixture(测试夹具)是指测试执行前必须准备好的固定环境——对象实例、数据库连接、文件系统状态等。JUnit5通过四个生命周期注解让开发者精准控制夹具的搭建与拆除时机:

  • @BeforeAll – 所有测试方法之前执行一次(static方法)
  • @AfterAll – 所有测试方法之后执行一次(static方法)
  • @BeforeEach – 每个@Test方法执行之前执行
  • @AfterEach – 每个@Test方法执行之后执行

这四个方法共同构成了最基本的Test Fixture机制,适用于初始化/清理资源、重置状态、建立Mock对象等场景。

关键概念: @BeforeAll / @AfterAll 必须修饰 static 方法(除非使用 @TestInstance(Lifecycle.PER_CLASS))。@BeforeEach / @AfterEach 通常是实例方法。

JUnit5 Test Fixture 执行顺序 JUnit5 基础 Test Fixture 执行顺序 类加载 @BeforeAll 对每个 @Test 方法重复 @BeforeEach @Test(测试方法) @AfterEach 下一个@Test @AfterAll 测试结束 注意:@BeforeAll → @BeforeEach → @Test → @AfterEach(循环)→ @AfterAll

3.2 实现流程:搭建第一个带Fixture的测试类

  1. 创建测试类,添加JUnit5依赖(org.junit.jupiter:junit-jupiter)。
  2. 声明实例变量,用于存放共享资源(如列表、计算器对象)。
  3. 定义静态方法,用@BeforeAll注解,初始化全局资源(如数据库连接)。
  4. 定义实例方法,用@BeforeEach注解,在每个测试前重置状态或创建新对象。
  5. 编写多个@Test方法,利用已准备好的Fixture进行断言。
  6. 定义@AfterEach方法,清理每个测试产生的临时数据。
  7. 定义@AfterAll静态方法,释放全局资源。

推荐模式: 将重量级资源(如连接池、Spring上下文)放在@BeforeAll一次性创建;轻量级资源(如Mock对象、临时文件)放在@BeforeEach中创建以保证隔离性。

3.3 代码实战:完整示例

以下演示一个简单的计算器测试,利用Fixture管理输入数据:

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {
    private Calculator calculator;

    @BeforeAll
    static void initAll() {
        System.out.println("=== @BeforeAll: 初始化全局配置 ===");
    }

    @AfterAll
    static void tearDownAll() {
        System.out.println("=== @AfterAll: 释放全局资源 ===");
    }

    @BeforeEach
    void setUp() {
        calculator = new Calculator();
        System.out.println("  @BeforeEach: 创建新的Calculator实例");
    }

    @AfterEach
    void tearDown() {
        System.out.println("  @AfterEach: 清理测试数据");
        calculator = null; // 显式置空,便于GC
    }

    @Test
    void testAdd() {
        assertEquals(5, calculator.add(2, 3));
        System.out.println("    @Test: testAdd");
    }

    @Test
    void testSubtract() {
        assertEquals(1, calculator.subtract(3, 2));
        System.out.println("    @Test: testSubtract");
    }
}

// 辅助类
class Calculator {
    int add(int a, int b) { return a + b; }
    int subtract(int a, int b) { return a - b; }
}

3.4 执行顺序验证(调试技巧)

运行上述测试,控制台输出顺序如下(已添加System.out标记):

=== @BeforeAll: 初始化全局配置 ===
  @BeforeEach: 创建新的Calculator实例
    @Test: testAdd
  @AfterEach: 清理测试数据
  @BeforeEach: 创建新的Calculator实例
    @Test: testSubtract
  @AfterEach: 清理测试数据
=== @AfterAll: 释放全局资源 ===

若使用IDE(IntelliJ IDEA / Eclipse),可在“Run”面板中观察测试树的执行顺序。也可通过JUnit5的TestWatcher或自定义扩展监听生命周期。

3.5 常见错误与避坑指南

错误场景原因正确做法
@BeforeAll方法不是staticJUnit5默认PER_METHOD生命周期要求静态方法添加static关键字,或使用@TestInstance(Lifecycle.PER_CLASS)
@AfterEach中未释放资源导致泄漏每个测试后残留文件描述符或网络连接在@AfterEach中关闭流、删除临时文件
测试间状态污染共享实例变量在@BeforeEach中未重新初始化在@BeforeEach中完全重置状态,不要依赖默认值
@AfterAll抛出异常清理代码异常导致整个测试类标记为失败使用try-catch包装清理逻辑,记录日志而不是抛出

警告: 永远不要在@BeforeAll或@AfterAll中执行耗时的IO操作(如读写大文件、网络请求),这会拖慢所有测试。对于重资源,考虑使用测试套件(@Suite)或参数化测试来减少重复初始化。

3.6 性能与稳定性优化建议

  • 复用重量级对象: 数据库连接、HTTP客户端等通过@BeforeAll创建一次,通过static字段共享,并用@AfterAll关闭。
  • 避免@BeforeAll中创建过多临时对象: 如果每个测试都需要独立副本,改用@BeforeEach,但通过@TestInstance(Lifecycle.PER_CLASS)配合@BeforeAll初始化不可变配置。
  • 使用断言描述信息: 在assertEquals等断言中传入消息,便于定位是哪个Fixture导致失败。
  • 避免在@AfterEach中执行断言: @AfterEach主要用于清理,不要在里面写断言逻辑,否则会干扰测试失败的原因分析。
  • 利用@DisplayName提高可读性: 为测试方法和生命周期方法添加@DisplayName,让测试报告更清晰。

最佳实践: 每个测试类只关注一个被测试单元,生命周期方法保持轻量。如果多个测试类需要相同Fixture,可抽取到抽象基类或使用@TestInstance + @ExtendWith扩展。