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 服务桩的任何实现都要能把运行证据重新映射到这张卡;如果更换支付供应商或部署平台后无法回答同一组问题,说明决定依赖的是工具偶然行为而不是模式语义。

结构解剖

Service Stub:测试时用替身代替外部服务订单服务(被测)payment.charge(¥597)不关心谁在响应PaymentGatewayinterface charge(amount)StubPayment(测试)StripePayment(生产)固定返回 success• 测试快速、可重复、不依赖网络 • 可模拟失败场景(超时、拒绝)• 前提:被测代码依赖接口而非具体服务(Separated Interface)Stub 代替外部服务返回固定结果,测试不依赖网络
Service Stub 用轻量替身代替外部服务,测试时返回固定结果, 消除对网络和外部系统的依赖,还可模拟失败场景。

选择与拒绝矩阵

评审问题选择 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《企业应用架构模式》与公开模式目录权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…