1. 理解TestRule接口
1.1 接口定义与设计意图
TestRule 是 JUnit 4.7 引入的核心接口,旨在替代 @Before / @After 注解的耦合式夹具管理,提供可复用、可组合、可装饰的测试环绕逻辑。其核心思想是装饰器模式:在测试方法执行前后插入自定义行为,而无需修改测试类本身。
设计目标对比:TestRule 与 @Before / @After 的差异决定了何时选择规则方案。
| 对比维度 | @Before / @After | TestRule |
|---|---|---|
| 复用性 | 继承或复制粘贴 | 声明式 @Rule,一次编写到处可用 |
| 组合能力 | 手动叠加 super 调用 | 多个 @Rule 自动链式装饰 |
| 异常处理 | 无法统一拦截异常 | 可在 evaluate() 中捕获/重抛/包装 |
| 资源管理 | 容易遗漏 finally | Statement 结构天然支持 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 可以嵌套组合。
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 的调试部署分为两步:单元验证和集成观察。
- 单元验证:直接编写测试来验证 Rule 自身行为。
public class TimerRuleTest {
@Rule
public TimerRule rule = new TimerRule();
@Test
public void should_record_elapsed_time() {
// 验证 TimerRule 不会阻塞测试执行
assertTrue(true);
}
}
- 断点调试:在
apply()和evaluate()行设置断点,以 Debug 模式运行测试。观察调用栈可清晰看到装饰器链的嵌套顺序。 - 日志插桩:在
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 字段不是 public | JUnit 忽略该 Rule | @Rule 字段必须为 public,非 static |
1.8 性能优化要点
- 轻量初始化:Rule 实例在测试类实例化时创建,避免在构造器或
before()中执行重资源操作(如建立网络连接),应使用延迟加载。 - 复用规则实例:若 Rule 无状态,可声明为
@ClassRule使其在整个测试类生命周期内共享,减少创建开销。 - 避免深层次嵌套:当
@Rule字段超过 5 个时,装饰链的 evaluate() 调用栈会变深,可考虑合并逻辑为一个复合 Rule。 - 精准过滤:在
before()中利用Description.getAnnotation()判断是否跳过某些测试,避免不必要的资源准备。