2、JUnit 5 核心注解:@Test、@BeforeEach、@AfterEach、@BeforeAll、@AfterAll 的使用与生命周期

写单元测试,说白了就是写一堆「小剧本」,让程序自己演一遍。而 JUnit 5 的这几个核心注解,就是用来编排这些剧本的「导演指令」。我刚开始用 JUnit 4 的时候,经常搞混 @Before 和 @BeforeClass,到了 JUnit 5 改名叫 @BeforeEach 和 @BeforeAll,语义清晰多了。

今天我们就来把这五个注解掰开揉碎,看看它们到底怎么用,生命周期又是怎么跑的。

2.1 @Test:最基础的「演员」

@Test 注解,是单元测试的入口。你想想看,一个方法加上 @Test,JUnit 就会把它当作一个测试用例来执行。

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

public class CalculatorTest {

    @Test
    void shouldAddTwoNumbers() {
        Calculator calc = new Calculator();
        int result = calc.add(3, 5);
        assertEquals(8, result, "3 + 5 应该等于 8");
    }

    @Test
    void shouldSubtractNumbers() {
        Calculator calc = new Calculator();
        int result = calc.subtract(10, 4);
        assertEquals(6, result);
    }
}

这里有两个 @Test 方法,它们之间是相互独立的。一个失败了,不影响另一个执行。我个人习惯每个测试方法只测一个逻辑点,这样出错了定位特别快。

小技巧: 测试方法名尽量用 should 开头,比如 shouldReturnTrueWhenInputIsValid。这样看测试报告就像读需求文档。

2.2 @BeforeEach 与 @AfterEach:每个用例的「化妆师」和「卸妆师」

@BeforeEach 标注的方法,会在每个 @Test 方法执行之前运行。@AfterEach 则是在每个 @Test 方法执行之后运行。说白了,就是给每个测试用例做「前置准备」和「后置清理」。

import org.junit.jupiter.api.*;

public class UserServiceTest {

    private UserService userService;
    private DatabaseConnection connection;

    @BeforeEach
    void setUp() {
        // 每个测试前,重新初始化服务
        connection = new DatabaseConnection("test-db");
        userService = new UserService(connection);
        System.out.println("准备测试环境...");
    }

    @AfterEach
    void tearDown() {
        // 每个测试后,清理数据
        connection.clearAllData();
        connection.close();
        System.out.println("清理测试环境...");
    }

    @Test
    void shouldCreateUser() {
        User user = userService.createUser("张三");
        assertNotNull(user.getId());
    }

    @Test
    void shouldFindUserById() {
        userService.createUser("李四");
        User found = userService.findUserById(1);
        assertEquals("李四", found.getName());
    }
}

我在项目中遇到过一个问题:多个测试用例共享同一个对象状态,结果第二个测试用例被第一个的「残留数据」影响了。后来我养成了习惯,在 @BeforeEach 里做完整的初始化,确保每个测试都是「干净」的。

避坑指南: 我曾经在 @BeforeEach 里创建了数据库连接,但在 @AfterEach 里忘记关闭。跑了几百个测试后,数据库连接池爆了。记住:@BeforeEach 开了什么资源,@AfterEach 一定要关掉。

2.3 @BeforeAll 与 @AfterAll:整个测试类的「总管」

@BeforeAll 在类中所有测试方法执行之前运行一次,@AfterAll 在所有测试方法执行之后运行一次。它们必须是静态方法(static)。

import org.junit.jupiter.api.*;

public class DatabaseTest {

    private static DataSource dataSource;

    @BeforeAll
    static void initDatabase() {
        // 只执行一次:启动测试数据库
        dataSource = new DataSource("jdbc:test://localhost:3306/mydb");
        dataSource.start();
        System.out.println("数据库已启动");
    }

    @AfterAll
    static void shutdownDatabase() {
        // 只执行一次:关闭测试数据库
        dataSource.stop();
        System.out.println("数据库已关闭");
    }

    @Test
    void testQuery1() {
        // 使用 dataSource 执行查询
    }

    @Test
    void testQuery2() {
        // 使用 dataSource 执行查询
    }
}

你想想看,如果每个测试都启动一次数据库,那跑 100 个测试就要启动 100 次,太慢了。所以像数据库连接、Redis 客户端、Spring 容器这种「重量级」资源,放在 @BeforeAll 里初始化最合适。

重要提醒: @BeforeAll 和 @AfterAll 必须是 static 方法。因为 JUnit 5 在执行它们时,还没有创建测试类的实例。这是很多新手容易犯的错。

2.4 生命周期执行顺序

搞清楚了每个注解的作用,我们来看看它们完整的执行顺序。假设有一个测试类,包含两个 @Test 方法:

步骤 执行内容 说明
1 @BeforeAll 类级别初始化,只执行一次
2 @BeforeEach 第一个测试用例的前置准备
3 @Test (方法1) 执行第一个测试
4 @AfterEach 第一个测试用例的后置清理
5 @BeforeEach 第二个测试用例的前置准备
6 @Test (方法2) 执行第二个测试
7 @AfterEach 第二个测试用例的后置清理
8 @AfterAll 类级别清理,只执行一次

为什么会这样设计?因为 JUnit 要保证每个测试用例的独立性。@BeforeEach 和 @AfterEach 就像给每个测试用例搭了一个「沙盒」,互不干扰。

2.5 实战中的最佳实践

说了这么多理论,来点实际的。我在团队里推行了这么几条规则:

  • @BeforeAll 放「只初始化一次」的资源:比如数据库连接池、Spring 上下文、Mock 框架的全局配置。
  • @BeforeEach 放「每个测试都需要」的准备工作:比如创建测试对象、清空数据库表、重置 Mock 对象。
  • @AfterEach 必须清理「可能影响下一个测试」的状态:比如文件句柄、网络连接、线程池。
  • @AfterAll 释放「重量级」资源:关闭数据库、停止嵌入式服务器。
我的习惯: 如果测试类里只有一个 @Test 方法,我依然会用 @BeforeEach 和 @AfterEach。因为未来可能会加新的测试方法,提前养成好习惯,省得后面改代码。

嗯,这里要注意一点:@BeforeEach 和 @AfterEach 的执行顺序,和它们在代码中的声明顺序无关。JUnit 5 会按照继承层次,从父类到子类依次执行。如果你有多个 @BeforeEach 方法,它们都会被执行,但顺序不保证。

我曾经踩过一个坑:在父类里用 @BeforeEach 初始化了 A 对象,在子类里又用 @BeforeEach 初始化了 B 对象,结果 B 的初始化依赖 A。后来我改成在子类的 @BeforeEach 里显式调用父类的初始化方法,才解决了问题。

最后总结一下:这几个注解是 JUnit 5 的基石。搞懂了它们的生命周期,你就能写出干净、独立、可重复的单元测试。下一章我们会聊断言和假设,到时候这些注解还会派上大用场。