4. Statement执行流程详解

在JUnit 4的TestRule体系中,Statement是整个测试执行的最小可调度单元。理解Statement.evaluate()的调用链、@Before/@After逻辑的插入时机以及异常如何沿调用链传播,是编写可靠自定义夹具的前提。本章从源码执行路径出发,逐层拆解这三个核心问题。

4.1 Statement 核心结构

Statement是一个抽象类,仅包含一个无参无返回值的方法:

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

每个TestRule的apply()方法都会接收一个Statement实例(即原始测试语句),并返回一个新的Statement(通常是匿名内部类或Lambda),在新语句的evaluate()中包裹前置/后置逻辑。

4.2 evaluate 方法调用链——嵌套包裹模型

JUnit 4使用装饰器模式构建调用链。每个TestRule像洋葱皮一样层层包裹原始测试语句。当最终evaluate()被触发时,调用顺序是从外到内执行前置逻辑,到达核心测试方法,再从内到外执行后置逻辑。

调用链执行口诀:最外层 Rule 的 before 最先执行,最外层 Rule 的 after 最后执行 —— 先进后出(LIFO)。

下面的SVG展示了由3个TestRule组成的典型调用链执行顺序:

Statement 嵌套调用链 evaluate() 执行流程 RuleA.apply() 外层包装 RuleB.apply() 中间包装 RuleC.apply() 内层包装 @Test 原始测试方法 异常沿调用链反向传播 ↑ Before 执行顺序 ① RuleA.before() → ② RuleB.before() → ③ RuleC.before() → ④ @Test After 执行顺序 ④ @Test → ③ RuleC.after() → ② RuleB.after() → ① RuleA.after() 关键规则 evaluate() 从外向内传递;异常从内向外传播;before/after 呈栈式 LIFO 顺序

4.3 before/after 逻辑插入点

在标准TestRule实现中,@Before和@After逻辑通过try/finally块插入到evaluate()的前后:

public Statement apply(Statement base, Description description) {
    return new Statement() {
        @Override
        public void evaluate() throws Throwable {
            // before 插入点 —— 在 base.evaluate() 之前执行
            System.out.println("[Rule] before - " + description.getMethodName());
            try {
                base.evaluate();  // 调用内层 Statement
            } finally {
                // after 插入点 —— 在 base.evaluate() 之后执行(无论成功或异常)
                System.out.println("[Rule] after - " + description.getMethodName());
            }
        }
    };
}
注意:finally块确保@After逻辑在@Test抛出异常时仍然执行。如果after逻辑本身也抛出异常,它会取代原始异常传播出去,这会导致原始异常被屏蔽——这是常见的陷阱之一。

插入点对比表:

插入点 代码位置 触发条件 异常影响
@Before 前置 try { } 之前 每次 evaluate() 调用 前置异常会阻止 base.evaluate() 执行
@After 后置 finally { } 内部 无论成功或异常 后置异常会覆盖原始异常(需警惕)
@AfterClass 类级 由 ClassRule 管理 所有测试方法执行完毕后 类级异常不影响已执行的方法

4.4 异常传播路径

异常在Statement调用链中的传播遵循反向路径:内层抛出的异常依次经过外层catch或finally块,最终到达JUnit框架的RunNotifier。有四种典型情况:

  1. 无异常路径:所有before执行 → base.evaluate()成功 → 所有after执行 → 正常返回。
  2. @Test 抛出异常:跳过后置before(已执行完),触发各层finally中的after,异常沿调用链向上传播。
  3. before 抛出异常:当前层的base.evaluate()不会执行,但finally中的after仍然执行(若存在),异常继续外传。
  4. after 抛出异常:如果@Test已经失败,finally中的after异常会替换原始测试异常,导致原始失败信息丢失。
⚠️ 异常屏蔽陷阱:当@Test和@After同时抛出异常时,只有@After的异常会被JUnit捕获。@Test的原始异常会被抑制。解决方案:在finally中使用Throwable.addSuppressed()添加原始异常,或使用ExpectedException规则来明确断言。

4.5 自定义Rule中的异常传播控制

一个健壮的TestRule应该正确处理异常传播,避免信息丢失:

public Statement apply(Statement base, Description description) {
    return new Statement() {
        @Override
        public void evaluate() throws Throwable {
            Throwable afterFailure = null;
            // before 逻辑
            System.out.println("[RobustRule] setup");
            try {
                base.evaluate();
            } catch (Throwable testFailure) {
                // 先保存测试异常
                afterFailure = testFailure;
                throw testFailure;  // 继续传播
            } finally {
                try {
                    // after 逻辑
                    System.out.println("[RobustRule] cleanup");
                } catch (Throwable cleanupFailure) {
                    if (afterFailure != null) {
                        // 将原始测试异常添加为抑制异常
                        cleanupFailure.addSuppressed(afterFailure);
                    }
                    throw cleanupFailure;
                }
            }
        }
    };
}

4.6 调试与验证方法

要直观观察调用链顺序,可以在每个Rule中打印堆栈或方法名:

public class OrderedRule implements TestRule {
    private final String name;

    public OrderedRule(String name) {
        this.name = name;
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                System.out.println(">>> ENTER: " + name);
                Throwable caught = null;
                try {
                    base.evaluate();
                } catch (Throwable t) {
                    caught = t;
                    throw t;
                } finally {
                    System.out.println("<<< EXIT: " + name +
                        (caught != null ? " (with exception)" : ""));
                }
            }
        };
    }
}

// 使用示例:在测试类上应用多个 OrderedRule
@Rule
public OrderedRule outer = new OrderedRule("OUTER");

@Rule
public OrderedRule inner = new OrderedRule("INNER");

@Test
public void testChain() {
    System.out.println("=== 执行测试方法 ===");
}

执行后控制台输出顺序为:

>>> ENTER: OUTER
>>> ENTER: INNER
=== 执行测试方法 ===
<<< EXIT: INNER
<<< EXIT: OUTER

4.7 常见错误与性能优化

常见错误清单:
  • 忘记调用 base.evaluate():导致测试方法永远不会执行,测试永远通过(空成功)。
  • 在 finally 中抛出未检查异常:覆盖原始异常,使调试信息丢失。
  • before 逻辑过重:在每条测试方法前创建数据库连接或加载大文件,应改为类级@ClassRule或@BeforeClass。
  • 多个 Rule 依赖顺序:如果 Rule 之间有隐式依赖,必须显式指定@Rule字段声明顺序(JUnit按字段声明顺序应用)。
性能优化建议
场景 问题 优化方案
每个测试都创建资源 频繁 I/O 或网络开销 改用 @ClassRule + 单例资源
Rule 内部持有重量级对象 内存泄漏 在 after 中显式释放,或在 finalize() 中兜底
Rule 链路过长(>5层) 调用栈过深,调试困难 合并同类别 Rule,或使用 RuleChain 显式排序
异常被屏蔽 丢失测试失败根因 使用 addSuppressed() 或 ErrorCollector 规则

4.8 与 RuleChain 的协同

JUnit 提供RuleChain来显式控制多个TestRule的包裹顺序:

@Rule
public RuleChain chain = RuleChain
    .outerRule(new OrderedRule("DB_CONNECTION"))
    .around(new OrderedRule("TRANSACTION"))
    .around(new OrderedRule("LOGGING"));

// 执行顺序: DB_CONNECTION.before → TRANSACTION.before → LOGGING.before
//           → @Test → LOGGING.after → TRANSACTION.after → DB_CONNECTION.after

使用RuleChain可以避免依赖字段声明顺序的隐式契约,使调用链意图更加清晰。