JUnit TestRule夹具分层教程 - 第1节

1.1 为什么需要 TestRule

在 JUnit 4 中,@Before 和 @After 提供了基本的测试夹具生命周期方法,但它们存在三个明显的局限性:不可复用——相同的前置/后置逻辑无法跨测试类共享;不透明——无法感知测试方法的元数据(如方法名、标签);不可组合——多个夹具逻辑只能平铺在同一个方法中,难以独立扩展。

TestRule 正是为解决上述问题而生的扩展点。它采用 装饰器模式(Decorator Pattern),将测试方法包装在一个可嵌套的 Statement 中,允许开发者在测试执行前后插入任意逻辑,同时保持代码的复用性与可组合性。

核心设计思想:TestRule 本质上是一个“环绕通知”——它不修改测试方法本身,而是在方法执行的外层包裹一层自定义行为,且多个 Rule 可以像洋葱一样层层嵌套。

1.2 核心接口解析

1.2.1 Statement —— 测试步骤的抽象封装

Statement 是 JUnit 中表示“一段可执行测试步骤”的抽象类,它是整个 Rule 机制的载体。源码极简,仅包含一个核心方法:

public abstract class Statement {
    public abstract void evaluate() throws Throwable;
}
  • evaluate():执行被包装的测试逻辑。在无 Rule 介入时,它直接调用 @Test 方法;被 Rule 包装后,evaluate() 的调用链会层层传递。
  • 不可直接实例化:框架或自定义 Rule 通过创建匿名子类来提供具体实现。

1.2.2 TestRule —— 扩展契约接口

TestRule 是 JUnit 4 中所有 Rule 必须实现的接口,只声明了一个方法:

public interface TestRule {
    Statement apply(Statement base, Description description);
}
参数/返回 说明
base 原始测试步骤,即未被当前 Rule 包装前的 Statement。Rule 必须返回一个新的 Statement,并在新 Statement 的 evaluate() 中调用 base.evaluate()。
description 测试方法的元数据描述对象,包含方法名、注解、测试类等信息,可用于条件判断或日志记录。
返回值 包装后的新 Statement。框架最终执行的是返回值中的 evaluate() 方法。

接口背后的核心约定:每个 Rule 必须返回一个 Statement,且该 Statement 的 evaluate() 必须最终调用 base.evaluate(),否则测试将无法正常执行。

1.2.3 BaseRule —— 简化开发的抽象基类

虽然 JUnit 4 官方未提供名为 BaseRule 的类,但在实际工程中,我们通常定义一个抽象基类来消除重复的匿名 Statement 创建代码。BaseRule 实现了 TestRule 接口,并提供 before() 和 after() 两个钩子方法:

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

    /**
     * 在测试方法执行前调用。子类可重写此方法添加前置逻辑。
     */
    protected void before(Description description) {
        // 默认无操作
    }

    /**
     * 在测试方法执行后调用。无论测试成功或失败,都会执行。
     * 子类可重写此方法添加后置清理逻辑。
     */
    protected void after(Description description) {
        // 默认无操作
    }
}
用途:BaseRule 将“前置处理 → 执行测试 → 后置处理”的三段式模板固化在 apply() 方法中,开发者只需重写 before() 和 after() 即可实现自定义 Rule,无需每次都编写匿名 Statement 类。

1.2.4 核心组件对比

组件 类型 职责 关键方法
Statement 抽象类 封装可执行的测试步骤 evaluate()
TestRule 接口 定义环绕逻辑的扩展契约 apply(Statement, Description)
BaseRule 抽象类 提供 before/after 模板方法,简化 Rule 开发 before(), after()
@Rule 注解 将 Rule 实例绑定到测试类的字段上 —
TestRule 核心架构与执行流程 声明层(测试代码) 接口层(框架核心) 实现层(扩展定制) 测试方法 @Test @Rule 注解 TestRule 接口 apply(Statement, Description) Statement 类 evaluate() BaseRule 基类 自定义 Rule 实现 声明 字段类型为 TestRule 创建包装 实现 继承 核心机制 TestRule 通过 @Rule 注解绑定到测试类字段,在 evaluate() 前后插入自定义逻辑,且多个 Rule 可层层嵌套组合。

1.3 @Rule 注解用法

@Rule 是 JUnit 4 提供的字段级注解,用于将 TestRule 实例绑定到测试类中。它必须满足以下规则:

  • 修饰字段:只能注解在 public 非静态字段 上(JUnit 4 要求字段为 public)。
  • 字段类型:字段类型必须实现 TestRule 接口。
  • 实例化时机:每个测试方法执行前,JUnit 会为测试类创建新的实例,因此 Rule 字段也会被重新实例化。
  • 多个 Rule:一个测试类可以声明多个 @Rule 字段,框架会按字段声明顺序依次应用 Rule。

基本使用示例:

public class TimerRuleTest {

    @Rule
    public TimerRule timerRule = new TimerRule();   // 字段必须为 public

    @Rule
    public TestName testName = new TestName();      // JUnit 内置 Rule,获取测试方法名

    @Test
    public void should_calculate_sum() {
        System.out.println("当前测试方法: " + testName.getMethodName());
        // 业务测试逻辑
    }

    @Test
    public void should_calculate_product() {
        System.out.println("当前测试方法: " + testName.getMethodName());
        // 业务测试逻辑
    }
}
技巧:多个 @Rule 字段的执行顺序遵循 字段声明顺序(由类中字段的原始定义顺序决定),外层 Rule 先包装,内层 Rule 后包装。若想控制严格顺序,应显式声明先后。

1.4 自定义 TestRule 实现步骤

下面通过一个完整的 计时 Rule(TimerRule) 演示从零实现自定义 TestRule 的完整过程。

步骤一:选择实现方式

  • 方式 A(直接实现 TestRule):适用于需要精细控制 Statement 包装逻辑的场景。
  • 方式 B(继承 BaseRule):适用于只需 before/after 钩子的简单场景,推荐优先使用。

步骤二:编写 Rule 类

方式 A:直接实现 TestRule 接口

public class TimerRule implements TestRule {
    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                long start = System.currentTimeMillis();
                try {
                    base.evaluate();
                } finally {
                    long elapsed = System.currentTimeMillis() - start;
                    System.out.printf("[%s] 耗时: %d ms%n",
                            description.getMethodName(), elapsed);
                }
            }
        };
    }
}

方式 B:继承 BaseRule(推荐)

public class TimerRule extends BaseRule {
    @Override
    protected void before(Description description) {
        // 在 ThreadLocal 或字段中记录开始时间
        TestContextHolder.setStartTime(System.currentTimeMillis());
    }
    @Override
    protected void after(Description description) {
        long start = TestContextHolder.getStartTime();
        long elapsed = System.currentTimeMillis() - start;
        System.out.printf("[%s] 耗时: %d ms%n",
                description.getMethodName(), elapsed);