JUnit TestRule夹具分层教程 - 第1节
1.1 为什么需要 TestRule
在 JUnit 4 中,@Before 和 @After 提供了基本的测试夹具生命周期方法,但它们存在三个明显的局限性:不可复用——相同的前置/后置逻辑无法跨测试类共享;不透明——无法感知测试方法的元数据(如方法名、标签);不可组合——多个夹具逻辑只能平铺在同一个方法中,难以独立扩展。
TestRule 正是为解决上述问题而生的扩展点。它采用 装饰器模式(Decorator Pattern),将测试方法包装在一个可嵌套的 Statement 中,允许开发者在测试执行前后插入任意逻辑,同时保持代码的复用性与可组合性。
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) {
// 默认无操作
}
}
1.2.4 核心组件对比
| 组件 | 类型 | 职责 | 关键方法 |
|---|---|---|---|
| Statement | 抽象类 | 封装可执行的测试步骤 | evaluate() |
| TestRule | 接口 | 定义环绕逻辑的扩展契约 | apply(Statement, Description) |
| BaseRule | 抽象类 | 提供 before/after 模板方法,简化 Rule 开发 | before(), after() |
| @Rule | 注解 | 将 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);