← 返回题目列表

模板方法模式如何做父类流程和子类实现的测试?

高频 中等 第 7 / 26 题 更新于 2026/08/02
模板方法模式单元测试契约测试继承

简化版

模板方法模式测试要分两层:父类流程测试和子类契约测试。父类流程测试验证模板步骤顺序、异常处理、资源释放和钩子调用;子类契约测试验证每个子类是否满足抽象步骤要求,例如不返回 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 仍被调用。
  • 追问:测试困难说明什么? 可能是父类职责太重、钩子太多或抽象边界不稳。

九、加强记忆

  1. 模板方法要测父类流程和子类契约两层。
  2. 父类流程用测试子类记录调用顺序。
  3. 异常路径必须验证 cleanup 和 onFailure。
  4. 钩子方法要测默认行为和覆盖行为。
  5. 子类要满足返回值、异常和副作用契约。
  6. 测试困难往往暴露模板抽象问题。