18.10 服务桩
以可控替身移除测试对缓慢、不可用或不可预测服务的依赖。
学习目标
- 能解释服务桩如何隔离测试与缓慢、昂贵或不稳定的外部服务,并画出被测代码、接口、替身和真实服务的边界
- 能编写 TypeScript 服务桩,覆盖成功、拒绝和超时等结果,同时保持与真实服务一致的契约
- 能根据测试价值、漂移风险、故障可观测性和维护成本,判断何时采用或拒绝服务桩
为什么 18.10 服务桩 值得单独学习
以可控替身移除测试对缓慢、不可用或不可预测服务的依赖。18.10 服务桩的核心不是套用某个框架 API,而是回答:如何让测试在稳定边界内观察真实业务决定,同时保留外部依赖的失败语义。在订单系统基础设施替换中,如果无法说清责任、状态和失败由谁承担,即使测试全绿,架构决定也没有完成。
↡在测试或演练中实现同一服务接口、由测试控制返回结果的替身。的价值不是“所有外部调用都返回成功”,而是把成功、拒绝、超时等可验证结果集中到测试装配中。本页以 2024 年中文版公开目录限定 18.10 服务桩 的范围,并依据 Martin Fowler 的作者图书页和模式目录独立重写。它不复现原书正文、插图或代码;目录只决定“要讲什么”,这里的案例、实验、判断题和答案均为本课程原创。
先建立直觉:问题、机制与代价
18.10 服务桩面对的具体压力是“外部服务延迟、费用、不可用性、测试重复性与故障覆盖率”。它采用的机制可以概括为:被测代码依赖稳定接口,测试装配把接口替换成可控实现,测试用例明确选择返回结果。机制带来的收益必须与替身维护、契约漂移和过度简化错误响应的成本同时记录,否则学习者只会得到一个没有拒绝条件的模式名称。
对 18.10 服务桩,优先比较的替代路线是:真实沙盒、集成测试、录制回放或更高层的端到端测试。只有在需要快速、可重复地覆盖边界结果,且有 ↡用同一组输入输出和错误约束同时验证真实服务与替身的测试。 防止漂移时,才值得引入服务桩;若实验只能显示结果而不能指出故障从何处被注入,就不能据此选择 18.10 服务桩。
目录单元到教学证据
18.10 服务桩
在 18.10 服务桩 的学习边界里,18.10 服务桩不是待背诵的目录词,而是用来检查“被测代码”是否只依赖接口、测试是否拥有故障注入权。对订单系统基础设施替换,学习者要记录依赖方向、结果分类和测试隔离的可观察变化,并说明它何时支持或否定 18.10 服务桩。
专属设计案例:订单支付网关测试隔离
把订单支付网关测试隔离切成“被测结账服务 → 支付接口 → 测试服务桩或生产网关 → 支付结果”四个观察点。18.10 服务桩的设计草案必须写出谁拥有测试状态、谁决定返回结果、失败怎样传播,以及依赖方向、对象语义、配置、测试隔离、表示转换中哪个指标最先提示当前方案不再适用。
设计记录采用五个可换行字段:单元键为 poeaa24-pattern-50-service-stub;模式族为 base;裁决是“能模拟成功、边界和故障响应,验证契约一致,并避免替身与真实服务长期漂移”;观测项包括依赖方向、对象语义、配置、测试隔离、表示转换;拒绝条件是“替身无法表达真实失败语义,或没有契约测试约束漂移”。
配置不是生产框架语法,而是一张评审卡。对 18.10 服务桩的任何实现都要能把运行证据重新映射到这张卡;如果更换支付供应商或部署平台后无法回答同一组问题,说明决定依赖的是工具偶然行为而不是模式语义。
结构解剖
选择与拒绝矩阵
| 评审问题 | 选择 18.10 服务桩 的证据 | 应拒绝或改用其他方案的信号 |
|---|---|---|
| 责任 | 测试装配拥有替身状态,被测代码只调用接口 | 业务代码判断当前运行在测试还是生产 |
| 变化 | 成功、拒绝和超时可被快速、稳定地重复 | 替身行为与真实网关长期分叉且无人校验 |
| 失败 | 每类外部失败都能映射到明确业务结果 | 替身只会返回成功,掩盖重试与补偿缺陷 |
| 替代 | 已与沙盒、集成测试和录制回放比较覆盖成本 | 以服务桩取代所有真实集成验证 |
代码实践:可控地替换支付网关
订单服务只依赖 PaymentGateway,测试通过服务桩控制支付结果。↡服务桩主动产生的、用于验证边界逻辑的超时、拒绝或限额等异常结果。必须与真实网关的错误分类保持一致,否则快速测试会给出虚假的安全感。
type PaymentRequest = {
orderId: string;
amountCents: number;
};
type PaymentResult = {
status: "approved" | "declined" | "timed-out";
transactionId?: string;
reason?: string;
};
interface PaymentGateway {
authorize(request: PaymentRequest): PaymentResult;
}
class StubPayment implements PaymentGateway {
constructor(private readonly result: PaymentResult) {}
authorize(_request: PaymentRequest): PaymentResult {
return this.result;
}
}
class Checkout {
constructor(private readonly gateway: PaymentGateway) {}
pay(orderId: string, amountCents: number): PaymentResult {
return this.gateway.authorize({ orderId, amountCents });
}
}
const approved = new Checkout(
new StubPayment({ status: "approved", transactionId: "stub-1" }),
).pay("order-7", 59700);
const declined = new Checkout(
new StubPayment({ status: "declined", reason: "limit exceeded" }),
).pay("order-8", 59700);
const timedOut = new Checkout(
new StubPayment({ status: "timed-out", reason: "gateway timeout" }),
).pay("order-9", 59700);这段代码让每个测试明确选择一种支付结果,既不等待网络,也不把拒绝和超时伪装成成功。生产装配可以传入真实网关;契约测试应使用同一组请求和结果断言,检查服务桩没有偏离真实网关的语义。
常见误区
可验证练习
练习
问题 1:覆盖边界。 StubPayment 只返回 approved,如何验证支付拒绝时订单不会标记为已支付?
问题 2:防止漂移。 真实支付网关把 timed-out 改名为 retryable-timeout,服务桩仍使用旧值。应如何处理?
问题 3:选择替代方案。 一个测试要验证供应商签名、TLS 和真实 HTTP 序列,服务桩是否足够?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 服务桩
在测试或演练中实现同一服务接口、由测试控制返回结果的替身。
- 契约测试
用同一组输入输出和错误约束同时验证真实服务与替身的测试。
- 可控故障
服务桩主动产生的、用于验证边界逻辑的超时、拒绝或限额等异常结果。
本章小结
掌握 18.10 服务桩的标志不是记住定义,而是能在订单支付网关测试隔离中解释“被测结账服务 → 支付接口 → 测试服务桩或生产网关 → 支付结果”的责任链,利用依赖方向、对象语义、配置、测试隔离、表示转换作出可证伪的选择,并在服务桩不能表达真实失败语义或缺少契约测试时明确拒绝。
前后导航
来源与改写范围
- Martin Fowler 作者图书页:核对全书主题、教程与模式参考结构。
- Martin Fowler 模式目录:核对模式名称、所属模式族和作者公开摘要。
- Pearson 出版社页面:交叉核对英文版出版信息与目录范围。