4. 测试夹具的生命周期管理:@Before、@After、@BeforeClass、@AfterClass、@Rule

4.1 原理:JUnit4 生命周期钩子与 Rule 机制

测试夹具(Test Fixture)的生命周期管理是自动化测试可靠性的基石。JUnit4 通过四个注解(@Before、@After、@BeforeClass、@AfterClass)和 @Rule 机制,为开发者提供了精确控制资源初始化与清理的能力。其核心执行顺序如下图所示:

生命周期执行顺序(单测试方法)

  1. 静态夹具:@BeforeClass(类级别,仅一次)
  2. 实例夹具前置:@Rule 的 apply() 开始包裹
  3. 实例夹具前置:@Before(每个测试方法前执行)
  4. 测试方法体:@Test
  5. 实例夹具后置:@After(每个测试方法后执行)
  6. 实例夹具后置:@Rule 的 apply() 结束包裹
  7. 静态夹具:@AfterClass(类级别,仅一次)
测试夹具生命周期执行流程 @BeforeClass @Rule (apply 开始) @Before @Test (测试方法) 具体测试逻辑 @After @Rule (apply 结束) @AfterClass 说明: @BeforeClass / @AfterClass 仅执行一次,方法必须是 static。 说明: @Rule 包裹整个测试方法, apply() 在 @Before 之前开始, 在 @After 之后结束。

4.2 实现流程

  1. 定义静态夹具方法:使用 @BeforeClass 和 @AfterClass 标注静态方法,用于全局资源(如数据库连接、Mock 服务器)的创建与销毁。
  2. 定义实例夹具方法:使用 @Before 和 @After 标注普通方法,在每个测试方法前后执行,用于测试数据重置、临时文件创建等。
  3. 引入 Rule:在测试类中声明 public TestRule 或 MethodRule 成员变量并标注 @Rule,实现自定义的行为包装(如超时控制、异常日志、重复执行)。
  4. 分离测试类与测试方法:将夹具逻辑集中在基类或 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 测试类与方法分离设计思路

将夹具管理职责从具体测试方法中剥离,通常有两种实践:

  1. 基类模式:定义抽象基类 BaseInstrumentedTest,内置 @BeforeClass/@AfterClass 和通用 Rule,子类只关注测试逻辑。
  2. 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() 提供了更细粒度的控制。