模板方法模式如何做父类流程和子类实现的测试?
简化版
模板方法模式测试要分两层:父类流程测试和子类契约测试。父类流程测试验证模板步骤顺序、异常处理、资源释放和钩子调用;子类契约测试验证每个子类是否满足抽象步骤要求,例如不返回 null、不吞异常、不破坏输入输出约定。只测某个具体子类的 happy path 不够,因为模板方法的风险在父子协作边界。
详细版
模板方法模式的测试重点不是“子类方法能不能跑”,而是父类是否始终控制住流程。
父类流程可以用测试子类记录调用顺序:
class TestTemplate extends AbstractJob {
List<String> calls = new ArrayList<>();
protected void doExecute() { calls.add("doExecute"); }
protected void after() { calls.add("after"); }
}
断言:
before -> doExecute -> after -> cleanup
每个真实子类还要测试自己的变化步骤是否符合父类契约,例如返回合法结果、异常按约定抛出、不会写共享状态。
记忆钩子:模板方法测试的重点是“父类剧本没被改,子类演员按契约演”。
完整版教学
面试提示:模板契约测试要证明“所有子类都遵守流程约束”,不是只测某一个 happy path。
一、为什么模板方法需要特殊测试
普通类测试关注自身逻辑;模板方法涉及父类和子类协作:
父类控制流程
子类实现步骤
父类继续处理结果
任何一边出问题,都可能导致整体行为错误。
二、父类流程测试验证步骤顺序
可以构造一个专用测试子类:
class RecordingJob extends AbstractJob {
final List<String> calls = new ArrayList<>();
protected void validate() { calls.add("validate"); }
protected void doExecute() { calls.add("doExecute"); }
protected void after() { calls.add("after"); }
}
执行模板入口后断言顺序:
validate, doExecute, after, cleanup
这样能证明父类骨架稳定。
三、异常路径必须测试
模板方法常负责异常和清理,所以要测失败路径:
doExecute 抛异常
-> onFailure 被调用
-> cleanup 仍然执行
-> 异常按约定抛出
如果只测成功路径,很容易漏掉资源释放和失败回调问题。
四、钩子方法要测试默认行为
钩子方法通常有默认实现。要测试:
- 子类不覆盖钩子时流程正常;
- 子类覆盖钩子时被正确调用;
- 钩子返回 false 时分支跳过;
- 钩子异常是否按父类策略处理。
钩子越多,测试越重要。
五、子类契约测试验证变化步骤
每个子类要满足父类契约。例如 parse 方法:
不能返回 null
失败抛 ParseException
返回 Row 必须包含 id/name
不直接保存数据库
可以对所有子类跑同一套契约测试,确保它们都符合父类要求。
六、不要只测具体子类 happy path
只测:
CsvImporter 可以导入成功
是不够的。还要测:
- ExcelImporter 失败是否清理资源;
- JsonImporter 返回空数据父类如何处理;
- CsvImporter 异常是否触发失败日志;
- 所有 importer 是否都走统一校验。
模板方法的 bug 往往出现在边界和异常路径。
七、测试也能反向发现设计问题
如果测试需要大量 mock 父类内部细节,或每个子类都要写完全不同的测试,可能说明模板抽象不稳定。
测试困难常提示:
| 测试现象 | 设计信号 |
|---|---|
| 子类必须覆盖很多方法 | 模板过宽 |
| 父类断言复杂 | 流程不清晰 |
| 钩子组合爆炸 | 扩展点过多 |
| 很难构造上下文 | 父类依赖太重 |
八、常见误区与追问
- 误区:模板方法只测真实子类就够。 父类流程本身也要有独立测试。
- 误区:只测成功路径。 异常、清理、失败回调往往更关键。
- 误区:抽象父类没法测。 可以用测试子类或最小 fake 子类验证流程。
- 误区:子类实现不需要契约测试。 子类返回值和异常必须满足父类预期。
- 追问:如何测试步骤顺序? 用记录型测试子类保存调用序列并断言。
- 追问:如何测试 cleanup 一定执行? 让 doExecute 抛异常,断言 cleanup 仍被调用。
- 追问:测试困难说明什么? 可能是父类职责太重、钩子太多或抽象边界不稳。
九、加强记忆
- 模板方法要测父类流程和子类契约两层。
- 父类流程用测试子类记录调用顺序。
- 异常路径必须验证 cleanup 和 onFailure。
- 钩子方法要测默认行为和覆盖行为。
- 子类要满足返回值、异常和副作用契约。
- 测试困难往往暴露模板抽象问题。