第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 通常是实例方法。
3.2 实现流程:搭建第一个带Fixture的测试类
- 创建测试类,添加JUnit5依赖(
org.junit.jupiter:junit-jupiter)。 - 声明实例变量,用于存放共享资源(如列表、计算器对象)。
- 定义静态方法,用
@BeforeAll注解,初始化全局资源(如数据库连接)。 - 定义实例方法,用
@BeforeEach注解,在每个测试前重置状态或创建新对象。 - 编写多个
@Test方法,利用已准备好的Fixture进行断言。 - 定义
@AfterEach方法,清理每个测试产生的临时数据。 - 定义
@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方法不是static | JUnit5默认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扩展。