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 方法,它们之间是相互独立的。一个失败了,不影响另一个执行。我个人习惯每个测试方法只测一个逻辑点,这样出错了定位特别快。
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 里做完整的初始化,确保每个测试都是「干净」的。
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 里初始化最合适。
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 释放「重量级」资源:关闭数据库、停止嵌入式服务器。
嗯,这里要注意一点:@BeforeEach 和 @AfterEach 的执行顺序,和它们在代码中的声明顺序无关。JUnit 5 会按照继承层次,从父类到子类依次执行。如果你有多个 @BeforeEach 方法,它们都会被执行,但顺序不保证。
我曾经踩过一个坑:在父类里用 @BeforeEach 初始化了 A 对象,在子类里又用 @BeforeEach 初始化了 B 对象,结果 B 的初始化依赖 A。后来我改成在子类的 @BeforeEach 里显式调用父类的初始化方法,才解决了问题。
最后总结一下:这几个注解是 JUnit 5 的基石。搞懂了它们的生命周期,你就能写出干净、独立、可重复的单元测试。下一章我们会聊断言和假设,到时候这些注解还会派上大用场。