JUnit 基础注解:测试的生命周期管理
写单元测试的时候,我最怕什么?不是测试写不对,而是每次跑测试前都得手动初始化一堆对象、建数据库连接,跑完还得清理现场。JUnit 给我们准备了四个生命周期注解,加上最核心的 @Test,就能把测试流程安排得明明白白。
说白了,这几个注解就是让你在测试跑之前、之后、每个测试方法甚至整个测试类的级别上,做点“家务活”。
@Test —— 单元测试的“主角”
这个注解大家再熟悉不过了。任何一个方法,加上 @Test,JUnit 就会把它当作一个测试来执行。我个人习惯把测试方法命名成 shouldXxx_whenYyy 这种模式,一看就知道在测什么场景。
@Test
void shouldReturnZero_whenInputIsNegative() {
Calculator cal = new Calculator();
assertEquals(0, cal.abs(-5));
}
💡 小技巧:如果方法抛出了预期异常,可以在 @Test 上加个参数:@Test(expected = ArithmeticException.class)。JUnit 4 和 5 的方式不一样,后面章节我会细讲参数化测试,这里先记住基本用法。
@BeforeAll —— 只跑一次的前置准备
想想看,你需要在整个测试类运行前,加载一个配置文件、初始化一个数据库连接池,或者启动一个嵌入式服务器。这种“一次性的”工作就交给 @BeforeAll。
注意:这个方法必须是 static 的(JUnit 4 和 JUnit 5 默认都是)。为什么?因为它在实例创建之前就调用了。我曾经在一开始写 JUnit 4 时就踩过这个坑——忘了写 static,结果每次跑测试都报错,找了半天才发现。
@BeforeAll
static void setUpClass() {
// 初始化全局资源
System.out.println("全局准备:只执行一次");
}
@BeforeEach —— 每次测试前的“清洁工”
这个注解是出镜率最高的。每个 @Test 方法执行之前,都会先执行 @BeforeEach 方法。我一般用它来做:创建测试对象、重置计数器、清空临时数据。
🔑 核心价值:确保每个测试在干净、独立的环境中运行,互不干扰。这比在测试方法里手动 new 对象要优雅得多。
@BeforeEach
void setUp() {
userService = new UserService();
cache = new HashMap<>();
}
@AfterEach —— 测试后的“收尾”
跑完每个测试后,需要清理脏数据、关闭流或打印日志。我印象最深的是有一次写文件操作测试,没用 @AfterEach 删除临时文件,结果磁盘被跑满了……嗯,从那以后,凡是涉及资源的测试,我一定要加上 @AfterEach 做清理。
@AfterEach
void tearDown() {
tempFile.delete(); // 确保文件被移除
userService.clearCache();
}
@AfterAll —— 整个测试类跑完后的“保洁阿姨”
跟 @BeforeAll 对称,这个方法在整个测试类的所有测试方法结束之后,调用一次。关闭数据库连接、销毁线程池、停止 Mock 服务器,都在这里做。
@AfterAll
static void tearDownClass() {
embeddedServer.stop();
System.out.println("全局清理:所有测试完成");
}
五个注解的执行顺序 —— 一张表看明白
| 注解 | 执行次数 | 执行时机 | 是否 static |
|---|---|---|---|
| @BeforeAll | 1次 | 所有测试方法运行前 | 是(默认) |
| @BeforeEach | 每方法1次 | 每个 @Test 方法执行前 | 否 |
| @Test | 每方法1次 | 作为测试方法执行 | 否 |
| @AfterEach | 每方法1次 | 每个 @Test 方法执行后 | 否 |
| @AfterAll | 1次 | 所有测试方法运行后 | 是(默认) |
完整示例:猜猜看顺序
来一段真实项目里会看到的代码。看看控制台输出顺序,是不是和你预想的一样?
public class CalcTest {
@BeforeAll
static void initAll() {
System.out.println("1. @BeforeAll");
}
@BeforeEach
void init() {
System.out.println("2. @BeforeEach");
}
@Test
void testAdd() {
System.out.println("3. 测试加法");
}
@Test
void testSubtract() {
System.out.println("4. 测试减法");
}
@AfterEach
void tearDown() {
System.out.println("5. @AfterEach");
}
@AfterAll
static void tearDownAll() {
System.out.println("6. @AfterAll");
}
}
输出顺序:
1. @BeforeAll
2. @BeforeEach → 3. 测试加法 → 5. @AfterEach
2. @BeforeEach → 4. 测试减法 → 5. @AfterEach
6. @AfterAll
注意到没?@BeforeEach 和 @AfterEach 像两个括号,把每个测试方法夹在中间。而 @BeforeAll 和 @AfterAll 是在整个测试类的外围。
避坑指南
⚠️ 我曾经犯过的错:
- 把
@BeforeAll和@AfterAll写成非 static 方法,在 JUnit 4 里会直接忽略执行,而且不报错!测试怎么跑怎么绿,但资源根本没初始化,诡异得很。 @BeforeEach里修改了类的成员变量,原本以为每个测试都会重新 new 对象,结果因为@TestInstance(PER_CLASS)生命周期,成员变量会被复用——血泪教训。- 每个测试的清理工作放在
@AfterEach,但如果有测试中途抛异常,清理代码仍然会执行。这其实是好事,但如果你在清理里又抛异常,那原始测试异常就会被覆盖。JUnit 5 会同时报告两个异常,但在 JUnit 4 里可能只显示清理异常,让人摸不着头脑。
什么时候用哪个?一张决策逻辑图
- 只跑一次的资源建立/销毁 →
@BeforeAll/@AfterAll(比如启动内嵌数据库) - 每个测试独立的前置/后置 →
@BeforeEach/@AfterEach(比如重置 mock、清空集合) - 如果所有测试共享同一个状态(比如只读数据)→ 可以放在
@BeforeAll里一次性加载,但千万注意并发问题 - 如果测试方法间互相影响(比如有 static 变量)→ 尽可能用
@BeforeEach重置,或者用@TestInstance(Lifecycle.PER_METHOD)强制每个测试新实例