Step 3: Statement 封装与执行链 —— 装饰器模式在 TestRule 中的落地

3.1 核心机制:Statement 是执行链的唯一载体

JUnit 的 Statement 是一个抽象类,仅包含一个核心方法 evaluate()。整个测试的执行路径,从 @Before、@Test 到 @After,最终都被折叠成一个嵌套的 Statement 链。 TestRule 通过装饰器模式,将自身的逻辑包装在链条的某个层次中,从而控制 evaluate() 的调用时机。

关键认知: 框架内部并不会直接调用 @Test 方法,而是将其封装为最内层的 Statement。所有 TestRule 就像洋葱皮一样,一层层包裹这个核心,最终只调用最外层的 evaluate()。

3.2 装饰器模式的结构抽象

下面这张 SVG 图展示了 TestRule 如何利用装饰器模式构建执行链。核心节点包含:原始测试方法、基础 Statement、多个装饰器 Rule、以及最终的调用入口。

TestRule 装饰器链结构 装饰器 A 装饰器 B 装饰器 C Base Statement evaluate() 调用入口 runLeaf() → 触发整个链 装饰器包装层(每个 TestRule 生成一个装饰器 Statement) 执行顺序: 外部装饰器A.evaluate → 装饰器B.evaluate → 装饰器C.evaluate → Base.evaluate → @Test 每个装饰器可以选择在调用 base.evaluate() 前后插入自定义逻辑。

3.3 显式调用示例:Statement.evaluate()

下面的代码演示了 JUnit 5 框架内部典型地如何将 @Test 方法包装成 Statement,并让用户自定义的 TestRule 有机会包裹它。

// 框架内部简化实现:runLeaf
protected void runLeaf(Statement statement, Description description, RunNotifier notifier) {
    // statement 此时已是最内层(真实测试方法)
    // 但在此之前,框架已经将所有 TestRule 包裹成外层装饰器
    for (TestRule each : getTestRules(description)) {
        statement = each.apply(statement, description);
    }
    // 现在 statement 是最外层的装饰器链
    statement.evaluate();  // 触发整个执行链
}

用户自定义 TestRule 时,必须实现 apply(Statement base, Description description) 方法,并返回一个新的 Statement。这个新 Statement 在 evaluate() 中通过调用 base.evaluate() 将执行传递到下一层。

实现模板: 下面的代码展示了一个标准的装饰器式 TestRule,它在测试方法执行前后分别输出日志。
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class LoggingRule implements TestRule {
    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                // 前置逻辑 (Before 增强)
                System.out.println("[开始测试] " + description.getMethodName());
                try {
                    base.evaluate();  // 核心:调用下游 Statement
                } finally {
                    // 后置逻辑 (After 增强)
                    System.out.println("[结束测试] " + description.getMethodName());
                }
            }
        };
    }
}

3.4 装饰器模式的四个角色对比

角色对应 JUnit 元素职责
Component(抽象组件)Statement 抽象类定义 evaluate() 契约
ConcreteComponent(具体组件)框架内部生成的 BaseStatement 指向 @Test执行真实的测试逻辑
Decorator(抽象装饰器)TestRule 接口接收 Statement 参数,返回包装后的新 Statement
ConcreteDecorator(具体装饰器)用户实现的 LoggingRule / Timeout 等在 base.evaluate() 前后添加横切逻辑

3.5 实现步骤:手动构建测试执行链

  1. 创建基础组件: 定义一个 Statement 实例,其 evaluate() 方法直接调用 MethodInvoker.invoke() 执行 @Test 注解的方法。
  2. 收集装饰器: 通过反射或注解获取当前测试类上的所有 @Rule / @ClassRule 字段列表。
  3. 层层包裹: 按规则顺序(通常 @Rule 字段声明顺序)对 Statement 进行装饰:
    for (TestRule rule : rules) { statement = rule.apply(statement, description); }
  4. 触发链条: 调用最外层 statement.evaluate(),执行链自外向内逐层传递,最终到达 @Test 方法。
  5. 异常传递: 任何一层的 evaluate() 抛出异常,都会沿着调用栈向上传播,直到被最外层的 RunNotifier 捕获。

3.6 配置细节:@Rule 字段的注入时机

JUnit 4 的 BlockJUnit4ClassRunner 在 withRules() 方法中完成装饰链的组装。框架会先通过 getTestRules() 方法获取所有 TestRule 实例,然后按照以下顺序应用:

  • 字段顺序: 声明在类中靠前的 @Rule 字段,其 apply() 方法先被调用,成为更外层的包装。
  • 继承规则: 子类的 @Rule 字段会在父类规则之后被执行包裹,但位于 @Test 之前。
  • 固定规则: @ClassRule 作用在类级别,包裹整个测试类;@Rule 作用在实例级别,包裹每个测试方法。
注意顺序陷阱: 如果多个 TestRule 都需要对测试方法进行资源初始化/清理,错误的包裹顺序可能导致资源泄漏。例如,TemporaryFolder 应该在最外层包裹,才能确保在目录删除前其他规则已完成日志刷新。

3.7 调试方法:跟踪 evaluate() 调用栈

当执行链出现异常或顺序不符合预期时,通过打印调用栈可以直观地看到装饰器嵌套层次:

// 在每个装饰器 evaluate 入口加入:
new RuntimeException("=== 装饰器跟踪点 ===").printStackTrace();

// 输出示例(简化):
java.lang.RuntimeException: === 装饰器跟踪点 ===
    at LoggingRule$1.evaluate(LoggingRule.java:12)
    at TimeoutRule$1.evaluate(TimeoutRule.java:8)
    at BaseStatement.evaluate(BaseStatement.java:4)
    at org.junit.runners.model.Runner.run(Runner.java:45)

堆栈阅读要点: 从栈顶到栈底,越往上越外层;最底部的 Runner.run() 是框架的调用入口。如果发现某个装饰器的 evaluate() 始终不出现,说明该规则未被正确注入或 apply() 返回了错误的 Statement。

3.8 常见错误及优化

错误场景现象解决方案
忘记调用 base.evaluate()测试方法永远不会执行,测试直接通过(无断言失败)确保装饰器 evaluate() 内部显式调用了 base.evaluate()
多次调用 base.evaluate()测试方法被执行多次,可能产生资源竞争或状态污染使用 try-finally 确保 base.evaluate() 只被调用一次
异常被吞没测试失败时日志丢失,无法定位原因在 catch 块中重新抛出异常,或使用 Throwables.propagateIfPossible()
执行顺序颠倒资源初始化和清理错乱检查 @Rule 字段声明顺序,外层规则应在字段列表中靠前

3.9 性能优化建议

  • 轻量化装饰器: 避免在 apply() 方法中执行重量级 I/O 操作,因为 apply() 本身在测试准备阶段执行,不影响 evaluate() 时间。
  • 避免深链: 装饰器层次过多(超过 10 层)会增加调用栈深度,影响 JIT 优化。将多个简单装饰器合并为一个复合装饰器可以降低层次。
  • 缓存 Description 对象: 如果装饰器需要频繁读取 Description 元数据,可以在 apply() 阶段预先解析并缓存,减少 evaluate() 阶段的反射调用。