1. 理解TestRule接口

1.1 接口定义与设计意图

TestRule 是 JUnit 4.7 引入的核心接口,旨在替代 @Before / @After 注解的耦合式夹具管理,提供可复用、可组合、可装饰的测试环绕逻辑。其核心思想是装饰器模式:在测试方法执行前后插入自定义行为,而无需修改测试类本身。

设计目标对比:TestRule 与 @Before / @After 的差异决定了何时选择规则方案。
对比维度@Before / @AfterTestRule
复用性继承或复制粘贴声明式 @Rule,一次编写到处可用
组合能力手动叠加 super 调用多个 @Rule 自动链式装饰
异常处理无法统一拦截异常可在 evaluate() 中捕获/重抛/包装
资源管理容易遗漏 finallyStatement 结构天然支持 try/finally
执行顺序控制固定父类→子类装饰器顺序由字段声明顺序决定

TestRule 接口源码极其精简,仅包含一个方法:

public interface TestRule {
    Statement apply(Statement base, Description description);
}

该接口不依赖任何运行期上下文,所有行为通过 apply 返回的 Statement 对象表达。

1.2 apply 方法签名解析

apply 方法是整个规则的入口,理解其两个参数和返回值是自定义夹具的起点。

  • Statement base — 原始测试语句,封装了待执行的测试方法体。调用 base.evaluate() 即可触发真正的测试逻辑。
  • Description description — 测试方法元数据,包含测试类名、方法名、注解信息,用于实现条件过滤或日志标记。
  • 返回值 Statement — 新的 Statement 对象,可在 evaluate() 中嵌入前置/后置逻辑,并委托给 base.evaluate()。

典型的装饰模式实现模板:

@Override
public Statement apply(Statement base, Description description) {
    return new Statement() {
        @Override
        public void evaluate() throws Throwable {
            // 前置逻辑
            System.out.println("before: " + description.getMethodName());
            try {
                base.evaluate();            // 执行实际测试
            } finally {
                // 后置逻辑(确保释放资源)
                System.out.println("after: " + description.getMethodName());
            }
        }
    };
}
关键原则:apply 方法只负责“装饰”,不负责执行。真正的执行链条在 evaluate() 被调用时才会触发。这种延迟执行机制使得多个 Rule 可以嵌套组合。
TestRule 核心执行流程 TestRule 核心执行流程 JUnit Runner @Rule 注解扫描 apply() 装饰拦截 新 Statement 包装 evaluate() 调用链 前置 / 后置逻辑 节点说明 ① 测试框架启动,扫描测试类 ② 查找带 @Rule 的字段,获取实例 ③ 调用 apply(),传入原始 Statement ④ 创建装饰 Statement,包裹自定义逻辑 ⑤ 逐层调用 evaluate() 执行测试链 ⑥ 前置处理 → 真实测试 → 后置清理

1.3 Statement 对象作用

Statement 是 JUnit 定义的一个抽象类,其唯一方法 evaluate() 代表了一段可执行的测试逻辑。

public abstract class Statement {
    public abstract void evaluate() throws Throwable;
}

在 TestRule 机制中,Statement 扮演了装饰器载体的角色:

  • 原始 Statement 由 JUnit 内部生成,直接调用测试方法。
  • 每个 Rule 的 apply() 将原始 Statement 包裹进新的 Statement,形成洋葱结构。
  • 最外层 evaluate() 调用时,逻辑依次穿过所有装饰层,最终到达真实测试方法。
易错提示:如果 apply() 直接返回 base 而不做任何包装,该 Rule 等同于失效。必须返回新创建的 Statement 对象才能注入自定义逻辑。

1.4 BaseRule 基类用法

JUnit 官方未提供名为 BaseRule 的类,但在实际工程中,我们通常构建一个抽象基类来消除重复的 Statement 匿名类代码,并强制子类只关注 before() 和 after() 钩子方法——这就是模板方法模式的应用。

/**
 * 自定义 TestRule 抽象基类,提供 before/after 模板方法。
 * 子类只需重写这两个方法,无需处理 Statement 包装细节。
 */
public abstract class BaseRule implements TestRule {

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                before(description);
                try {
                    base.evaluate();
                } finally {
                    after(description);
                }
            }
        };
    }

    /**
     * 前置钩子:测试执行前调用。可获取测试描述信息做条件过滤。
     */
    protected void before(Description description) throws Throwable {
        // 默认空实现,子类按需重写
    }

    /**
     * 后置钩子:测试执行后调用,在 finally 块中保证执行。
     */
    protected void after(Description description) {
        // 默认空实现,子类按需重写
    }
}

子类示例——数据库连接规则:

public class DatabaseRule extends BaseRule {

    private Connection connection;

    @Override
    protected void before(Description description) throws Throwable {
        connection = DriverManager.getConnection("jdbc:h2:mem:test");
        System.out.println("数据库连接已创建 — " + description.getMethodName());
    }

    @Override
    protected void after(Description description) {
        if (connection != null) {
            try { connection.close(); } catch (SQLException ignored) { }
            System.out.println("数据库连接已关闭 — " + description.getMethodName());
        }
    }

    public Connection getConnection() {
        return connection;
    }
}
设计收益:通过 BaseRule 抽象,团队中所有自定义夹具统一了异常处理契约和生命周期结构,降低了 Rule 实现的心智负担。

1.5 完整示例:计时夹具规则

下面实现一个 TimerRule,自动记录每个测试方法的执行耗时,并输出到控制台。该示例完整展示了接口、基类、注解绑定的全流程。

public class TimerRule extends BaseRule {

    @Override
    protected void before(Description description) {
        // 存储开始时间到 ThreadLocal,避免线程冲突
        TimerContext.start(description.getMethodName());
    }

    @Override
    protected void after(Description description) {
        long elapsed = TimerContext.stop();
        System.out.printf("[Timer] %s.%s 耗时: %d ms%n",
                description.getClassName(),
                description.getMethodName(),
                elapsed);
    }
}

// 配套的测试类
public class UserServiceTest {

    @Rule
    public TimerRule timer = new TimerRule();

    @Test
    public void should_create_user() {
        // 测试逻辑...
        System.out.println("执行 should_create_user");
    }

    @Test
    public void should_update_user() {
        // 测试逻辑...
        System.out.println("执行 should_update_user");
    }
}

输出效果:

[Timer] UserServiceTest.should_create_user 耗时: 23 ms
[Timer] UserServiceTest.should_update_user 耗时: 17 ms

1.6 调试与验证

自定义 TestRule 的调试部署分为两步:单元验证和集成观察。

  1. 单元验证:直接编写测试来验证 Rule 自身行为。
public class TimerRuleTest {

    @Rule
    public TimerRule rule = new TimerRule();

    @Test
    public void should_record_elapsed_time() {
        // 验证 TimerRule 不会阻塞测试执行
        assertTrue(true);
    }
}
  1. 断点调试:在 apply() 和 evaluate() 行设置断点,以 Debug 模式运行测试。观察调用栈可清晰看到装饰器链的嵌套顺序。
  2. 日志插桩:在 before/after 中打印 description.getClassName() 和 description.getMethodName(),确认 Rule 被正确绑定到每个测试方法。
部署建议:将通用 Rule 类放在 src/test/common 包下,并通过 Maven/Gradle 的 test-jar 构件在多模块间共享。

1.7 常见错误与避坑

错误模式现象解决方案
apply() 返回 base 本身Rule 逻辑不生效必须返回新创建的 Statement 实例
after() 未放在 finally 中测试异常时资源泄漏使用 try { base.evaluate(); } finally { ... } 结构
多个 Rule 顺序依赖结果与预期不符Rule 执行顺序与字段声明顺序一致,不要假设先后
在 before() 中抛出非 RuntimeException测试类无法实例化before() 声明 throws Throwable,不要捕获后静默
Rule 字段不是 publicJUnit 忽略该 Rule@Rule 字段必须为 public,非 static

1.8 性能优化要点

  • 轻量初始化:Rule 实例在测试类实例化时创建,避免在构造器或 before() 中执行重资源操作(如建立网络连接),应使用延迟加载。
  • 复用规则实例:若 Rule 无状态,可声明为 @ClassRule 使其在整个测试类生命周期内共享,减少创建开销。
  • 避免深层次嵌套:当 @Rule 字段超过 5 个时,装饰链的 evaluate() 调用栈会变深,可考虑合并逻辑为一个复合 Rule。
  • 精准过滤:在 before() 中利用 Description.getAnnotation() 判断是否跳过某些测试,避免不必要的资源准备。