2. @BeforeEach 与 @AfterEach 基本用法:方法级别资源初始化、清理模式、异常处理

方法级别的生命周期回调是 JUnit 5 中最常用的夹具机制。每一组测试方法拥有独立的状态空间,通过 @BeforeEach 和 @AfterEach 可以精确控制每个测试用例的运行前后环境,避免测试间相互污染。

2.1 执行原理

对于单个测试方法,JUnit 执行顺序为:

  1. 调用构造器创建测试实例(测试类实例是每个方法独立的,取决于实例生命周期策略)
  2. 执行 @BeforeEach 标注方法
  3. 执行测试方法本体(含 @Test)
  4. 执行 @AfterEach 标注方法

每个测试方法都重复上述步骤,因此 @BeforeEach 和 @AfterEach 保证每个测试获取独立的资源,且无论测试是否通过,@AfterEach 都会被执行(除非 @BeforeEach 抛出异常导致实例状态不可用时的特殊情形)。

2.2 标准实现流程

典型场景:操作临时文件或数据库连接。下面演示一个简单的文件读写测试。

2.2.1 定义资源与夹具方法

public class FileProcessorTest {
    private File tempFile;
    private BufferedWriter writer;

    @BeforeEach
    void setUp() throws IOException {
        // 创建临时文件,每个测试方法独立
        tempFile = File.createTempFile("test_", ".txt");
        writer = new BufferedWriter(new FileWriter(tempFile));
    }

    @AfterEach
    void tearDown() throws IOException {
        // 关闭资源并删除临时文件
        if (writer != null) {
            writer.close();
        }
        if (tempFile != null && tempFile.exists()) {
            tempFile.delete();
        }
    }

    @Test
    void testWriteContent() throws IOException {
        writer.write("Hello JUnit");
        writer.flush();
        assertEquals(10, tempFile.length());
    }

    @Test
    void testEmptyFile() throws IOException {
        assertEquals(0, tempFile.length());
    }
}

每个测试方法执行前,setUp 都会创建新的临时文件和 BufferedWriter,执行后 tearDown 关闭流并清理文件,保证测试间无文件残留。

2.3 异常处理模式

当 @BeforeEach 抛出异常时,JUnit 会跳过当前测试方法的执行,但 @AfterEach 是否执行取决于异常类型与 JUnit 版本的行为逻辑。从 JUnit 5.8 开始,若 @BeforeEach 抛出检查型异常或运行时异常,后续的 @AfterEach 仍然执行;但在极早期版本中可能因异常导致实例不可用而跳过。推荐在 @AfterEach 中也捕获异常,确保资源释放。

注意:若 @BeforeEach 抛出异常,对应测试方法不会执行,但 @AfterEach 仍会被调用。如果 @AfterEach 本身抛出异常,则测试结果标记为失败,并抛出 AfterEachFailureException(JUnit 内部封装)。
@BeforeEach
void fragileSetUp() {
    // 假设某个资源初始化失败
    throw new RuntimeException("Failed to init");
}

@Test
void neverRun() {
    // 本测试不会执行
}

@AfterEach
void safeTearDown() {
    // 即使 setUp 失败,此方法仍运行
    System.out.println("Cleanup executed");
}

2.4 调试与验证执行顺序

在测试方法中添加日志输出,可以直观观察生命周期:

import java.util.logging.Logger;

public class LifecycleOrderTest {
    private static final Logger log = Logger.getLogger(LifecycleOrderTest.class.getName());

    @BeforeEach
    void setup() {
        log.info("--- BeforeEach ---");
    }

    @Test
    void testOne() {
        log.info("--- testOne ---");
    }

    @Test
    void testTwo() {
        log.info("--- testTwo ---");
    }

    @AfterEach
    void cleanup() {
        log.info("--- AfterEach ---");
    }
}

控制台输出示例:

--- BeforeEach ---
--- testOne ---
--- AfterEach ---
--- BeforeEach ---
--- testTwo ---
--- AfterEach ---

两个测试方法独立执行,各自拥有完整的 BeforeEach → Test → AfterEach 流程。

2.5 常见错误与陷阱

错误场景 后果 正确做法
@BeforeEach 中打开数据库连接,@AfterEach 中忘记关闭 资源泄漏,测试数量越多泄漏越严重 使用 try/finally 或 try-with-resources 确保关闭;若通过字段持有,在 @AfterEach 中主动关闭
@BeforeEach 抛出异常后,@AfterEach 检查字段为 null 但没有空判断 NullPointerException 掩盖真实失败原因 在 @AfterEach 中加 null 安全防护
将 @BeforeEach 误写成 @BeforeAll,导致资源在多个测试间共享 测试间状态冲突,产生不可重现的失败 明确区分方法级别与类级别,按需选择
在 @AfterEach 中抛出未捕获的异常 测试即使通过也失败,且资源可能未完全释放 在 @AfterEach 内部 catch 所有异常并记录日志,或委托给安全清理辅助方法

2.6 性能与稳定性优化

  • 避免重型操作:@BeforeEach 中不要启动外部进程、大规模计算或网络请求,若必须使用,考虑全局一次性初始化(@BeforeAll)或缓存。
  • 超时防护:在 @BeforeEach 或 @AfterEach 中使用 assertTimeout 或自定义超时逻辑,防止死锁或慢操作阻塞整个测试套件。
  • 资源池:对于昂贵资源(如数据库连接),可使用连接池并配合 @BeforeEach 从池中借用,@AfterEach 归还,避免频繁创建销毁。
  • 日志滚动:控制 @AfterEach 中的日志输出量,避免海量测试产生过大日志。

2.7 执行流程总览

下图展示了一个测试方法在正常与异常路径下的完整生命周期:

@BeforeEach / @AfterEach 执行流程 测试方法入口 @BeforeEach (成功) @Test 主体 @AfterEach 测试结束 @BeforeEach 抛出异常 跳过@Test,但仍执行@AfterEach 异常处理规则 • BeforeEach异常→跳过测试 • AfterEach仍会执行 • AfterEach异常→测试失败

2.8 小结

@BeforeEach 与 @AfterEach 是方法级别资源管理的基石。正确理解其异常传播机制、资源生命周期以及执行顺序,能够编写出稳定、无泄漏的测试夹具。下一级别将扩展至类级别(@BeforeAll / @AfterAll),实现跨测试的资源复用。