JUnit 基础注解:测试的生命周期管理

写单元测试的时候,我最怕什么?不是测试写不对,而是每次跑测试前都得手动初始化一堆对象、建数据库连接,跑完还得清理现场。JUnit 给我们准备了四个生命周期注解,加上最核心的 @Test,就能把测试流程安排得明明白白。

说白了,这几个注解就是让你在测试跑之前、之后、每个测试方法甚至整个测试类的级别上,做点“家务活”。

测试类生命周期执行流程 @BeforeAll(执行一次) @BeforeEach(每个测试前执行) @Test — 测试方法 每个测试方法 重复三行 @AfterEach(每个测试后执行) @AfterAll(执行一次) 循环执行N个测试 ⚠ @BeforeAll / @AfterAll 必须是 static 方法 在 JUnit 5 中 @BeforeAll 可以是非 static,但需要 @TestInstance(Lifecycle.PER_CLASS)

@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
@BeforeAll1次所有测试方法运行前是(默认)
@BeforeEach每方法1次每个 @Test 方法执行前否
@Test每方法1次作为测试方法执行否
@AfterEach每方法1次每个 @Test 方法执行后否
@AfterAll1次所有测试方法运行后是(默认)

完整示例:猜猜看顺序

来一段真实项目里会看到的代码。看看控制台输出顺序,是不是和你预想的一样?

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 里可能只显示清理异常,让人摸不着头脑。

什么时候用哪个?一张决策逻辑图

  1. 只跑一次的资源建立/销毁 → @BeforeAll / @AfterAll(比如启动内嵌数据库)
  2. 每个测试独立的前置/后置 → @BeforeEach / @AfterEach(比如重置 mock、清空集合)
  3. 如果所有测试共享同一个状态(比如只读数据)→ 可以放在 @BeforeAll 里一次性加载,但千万注意并发问题
  4. 如果测试方法间互相影响(比如有 static 变量)→ 尽可能用 @BeforeEach 重置,或者用 @TestInstance(Lifecycle.PER_METHOD) 强制每个测试新实例