4. 测试夹具的生命周期管理:@Before、@After、@BeforeClass、@AfterClass、@Rule
4.1 原理:JUnit4 生命周期钩子与 Rule 机制
测试夹具(Test Fixture)的生命周期管理是自动化测试可靠性的基石。JUnit4 通过四个注解(@Before、@After、@BeforeClass、@AfterClass)和 @Rule 机制,为开发者提供了精确控制资源初始化与清理的能力。其核心执行顺序如下图所示:
生命周期执行顺序(单测试方法)
- 静态夹具:
@BeforeClass(类级别,仅一次) - 实例夹具前置:
@Rule的apply()开始包裹 - 实例夹具前置:
@Before(每个测试方法前执行) - 测试方法体:
@Test - 实例夹具后置:
@After(每个测试方法后执行) - 实例夹具后置:
@Rule的apply()结束包裹 - 静态夹具:
@AfterClass(类级别,仅一次)
4.2 实现流程
- 定义静态夹具方法:使用
@BeforeClass和@AfterClass标注静态方法,用于全局资源(如数据库连接、Mock 服务器)的创建与销毁。 - 定义实例夹具方法:使用
@Before和@After标注普通方法,在每个测试方法前后执行,用于测试数据重置、临时文件创建等。 - 引入 Rule:在测试类中声明 public TestRule 或 MethodRule 成员变量并标注
@Rule,实现自定义的行为包装(如超时控制、异常日志、重复执行)。 - 分离测试类与测试方法:将夹具逻辑集中在基类或 Rule 中,避免在每个测试方法中重复编写资源管理代码。
4.3 代码示例:完整生命周期管理
import org.junit.*;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
public class FixtureLifecycleDemo {
private static DatabaseConnection dbConn;
private TemporaryFolder tempFolder;
private TestContext context;
// 类级别夹具
@BeforeClass
public static void globalSetup() {
dbConn = new DatabaseConnection("jdbc:mysql://localhost/testdb");
dbConn.connect();
System.out.println("[BeforeClass] 数据库连接已建立");
}
@AfterClass
public static void globalTeardown() {
dbConn.disconnect();
System.out.println("[AfterClass] 数据库连接已关闭");
}
// 自定义 Rule:日志记录
@Rule
public TestRule logRule = (base, description) -> new Statement() {
@Override
public void evaluate() throws Throwable {
System.out.println("[Rule] 开始执行: " + description.getMethodName());
try {
base.evaluate();
} finally {
System.out.println("[Rule] 结束执行: " + description.getMethodName());
}
}
};
// 超时 Rule(全局)
@Rule
public Timeout globalTimeout = Timeout.seconds(5);
// 实例夹具
@Before
public void setUp() {
tempFolder = new TemporaryFolder();
context = new TestContext();
System.out.println("[Before] 测试前准备完成");
}
@After
public void tearDown() {
tempFolder.delete();
context.clear();
System.out.println("[After] 测试后清理完成");
}
@Test
public void testInsertUser() {
// 使用 dbConn 和 tempFolder 执行插入测试
Assert.assertNotNull(dbConn);
Assert.assertTrue(tempFolder.getRoot().exists());
System.out.println("[Test] 测试: testInsertUser");
}
@Test
public void testDeleteUser() {
// 删除用户测试
System.out.println("[Test] 测试: testDeleteUser");
}
}
4.4 调试技巧:观察生命周期执行顺序
在 Android 测试中,可以通过 Logcat 或标准输出打印日志来验证执行顺序。推荐在 @BeforeClass、@Before、@Test、@After、@AfterClass 中分别添加日志,运行测试后观察输出。
// 预期日志顺序(单测试方法运行两次时):
[BeforeClass] 数据库连接已建立
[Rule] 开始执行: testInsertUser
[Before] 测试前准备完成
[Test] 测试: testInsertUser
[After] 测试后清理完成
[Rule] 结束执行: testInsertUser
[Rule] 开始执行: testDeleteUser
[Before] 测试前准备完成
[Test] 测试: testDeleteUser
[After] 测试后清理完成
[Rule] 结束执行: testDeleteUser
[AfterClass] 数据库连接已关闭
4.5 常见错误与避坑
| 错误模式 | 说明 | 解决方案 |
|---|---|---|
| 忽略静态修饰符 | @BeforeClass 和 @AfterClass 标注的方法不是 static。 |
必须添加 public static void 签名,否则 JUnit 会忽略。 |
| Rule 声明顺序依赖 | 多个 Rule 的执行顺序由声明顺序决定(自上而下),但不要依赖未定义的顺序。 | 使用 @RuleChain 或显式组合 Rule。 |
| 在 @Before 中创建全局资源 | 每个测试方法都重复创建数据库连接,导致性能退化。 | 将重量级资源移至 @BeforeClass。 |
| 忘记释放临时文件 | @After 未清理导致测试残留文件。 |
使用 TemporaryFolder Rule 自动清理。 |
| @Rule 与 @Before/@After 相互干扰 | Rule 内部可能修改测试上下文,影响 @Before 中初始化的变量。 | 确保 Rule 的 apply() 不修改测试实例状态,仅在前后执行日志或计时。 |
4.6 性能与稳定性优化
- 使用 @BeforeClass 缓存重量级对象:如数据库连接、Mock 服务器、大型缓存等,避免每个测试方法都重新创建。
- 避免在 @After 中执行耗时操作:如关闭网络连接、删除大量文件应在 @AfterClass 中统一处理,除非有明确隔离需求。
- 合理使用 @Rule 的 apply() 粒度:Rule 可以包裹整个测试方法,但不应在 apply 内部执行过于复杂的 I/O 或计算,否则会导致所有测试方法额外开销。
- 分离测试类与方法:将公共夹具逻辑提取到抽象基类或自定义 Rule 中,减少冗余代码,便于维护。
- 避免依赖执行顺序:测试之间不应共享可变状态(除了静态夹具),否则并行执行时会出现竞争条件。
最佳实践:在 Android 测试中,推荐使用 ActivityTestRule 或 ServiceTestRule 管理组件生命周期,它们天然实现了 @Rule 模式,可以自动完成 Activity 启动与销毁,避免手动管理 @Before/@After 中的组件调度。
4.7 生命周期注解对比表
| 注解 | 执行次数 | 方法签名 | 典型用途 |
|---|---|---|---|
@BeforeClass |
整个测试类运行前(1 次) | public static void method() |
打开数据库连接、启动 Mock 服务 |
@AfterClass |
整个测试类运行后(1 次) | public static void method() |
关闭数据库连接、停止 Mock 服务 |
@Before |
每个测试方法前(N 次) | public void method() |
重置测试数据、创建临时目录 |
@After |
每个测试方法后(N 次) | public void method() |
清理临时文件、释放个别资源 |
@Rule |
包裹每个测试方法(N 次) | public TestRule field |
超时控制、重试逻辑、日志注入 |
4.8 测试类与方法分离设计思路
将夹具管理职责从具体测试方法中剥离,通常有两种实践:
- 基类模式:定义抽象基类
BaseInstrumentedTest,内置@BeforeClass/@AfterClass和通用 Rule,子类只关注测试逻辑。 - Rule 模式:将夹具逻辑封装为自定义
TestRule,通过@Rule组合到测试类中,更加灵活。
// 基类模式示例
public abstract class BaseDatabaseTest {
@Rule
public DatabaseRule dbRule = new DatabaseRule();
@Before
public void baseSetup() {
dbRule.cleanTable("users");
}
}
public class UserDaoTest extends BaseDatabaseTest {
@Test
public void testInsert() {
// 直接使用 dbRule.getConnection() 进行测试
}
}
注意:在 Android 仪器化测试中,@BeforeClass 和 @AfterClass 必须位于测试类中,且不能被 @UiThreadTest 或 @ActivityTestRule 干扰。若使用 ActivityTestRule,其 beforeActivityLaunched() 和 afterActivityFinished() 提供了更细粒度的控制。