41 为编码测试
把测试当成代码的第一个用户和设计反馈源,从端到端切片推动接口、耦合与可观察性改善。
学习目标
- 能把用户可观察行为写成端到端测试,并从测试摩擦发现接口和依赖问题
- 能区分端到端、局部和性质证据的职责,不用行覆盖率代替行为合同
- 能用失败样本、可观察日志和回退动作重放测试结论,让测试成为设计反馈
测试是代码的第一个用户
本页依据 David Thomas、Andrew Hunt《程序员修炼之道:通向务实的最高境界(第2版)》,云风译,电子工业出版社,2020 年 4 月,ISBN 9787121384356 的公开中文目录,独立重构 41 为编码测试。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图或答案。
测试不只是发布前找 Bug 的筛子。它第一次调用接口、准备输入、观察结果并处理错误,因此测试写起来很费力,通常说明代码的依赖、边界或命名也很费力。把测试当成第一个用户,意味着先从真实行为开始,再让实现变得容易被调用、观察和恢复。
三个会让测试失去设计反馈的陷阱
从行为目标到可反馈实现
<Term def="从用户输入经过真实边界得到可观察结果、错误和副作用的最小行为路径。">端到端切片</Term>是本单元的起点。它不要求一次覆盖整个系统,而要求至少让一个真实行为从入口走到可验收结果。回路包含五个节点:
- 行为目标:说明谁在什么条件下期待什么结果。
- 测试接口:以调用者视角准备请求、身份、依赖和观察点。
- 最小实现:只实现让端到端场景前进一步的必要代码。
- 反馈:比较输出、错误、状态和副作用,定位首个差异。
- 扩展:增加边界、失败、权限和并发场景,再抽取局部测试。
41 与提示 66–70:为什么要从端到端开始
本单元对应 41 为编码测试,以及 提示 66:测试与找 Bug 无关、提示 67:测试是代码的第一个用户、提示 68:既非自上而下,也不自下而上,基于端对端构建、提示 69:为测试做设计、提示 70:要对软件做测试,否则只能留给用户去做。
这些提示共同表达一个工作顺序:先写一条用户可观察行为,测试会迫使我们决定接口、依赖和错误边界;实现从最小切片开始;局部测试随后提供更快反馈;失败记录反过来改变设计。测试的价值不是“找到了多少 Bug”,而是能否让系统的行为合同更清晰、更容易调用和复核。
<Term def="测试调用接口时所需的输入、身份、依赖、时钟和观察方式;它应该尽量接近真实调用者。">测试视角</Term>不是把内部方法暴露给测试。若只能构造复杂全局状态才能测试一个简单行为,摩擦本身就是设计反馈,应优先调整依赖边界。
让测试成为可用接口
<Term def="让调用者可以提供依赖、触发行为、读取结果并判断拒绝原因的结构特性。">可测试性</Term>来自清晰责任、显式依赖和可观察副作用,而不是大量公开内部函数。一个可测试接口通常能说明:谁调用、传入什么、成功返回什么、失败如何拒绝、状态何时提交。
type CheckoutRequest = {
userId: string;
itemIds: ReadonlyArray<string>;
};
type CheckoutResult =
| { kind: "accepted"; orderId: string }
| { kind: "rejected"; reason: "empty-cart" | "not-authorized" };
async function checkout(request: CheckoutRequest): Promise<CheckoutResult> {
if (request.itemIds.length === 0)
return { kind: "rejected", reason: "empty-cart" };
return { kind: "accepted", orderId: `order-${request.userId}` };
}测试可以从调用者视角检查空购物车和成功订单,也能看到拒绝原因,而不必读取函数内部变量。真实实现还要注入库存、授权和订单存储;接口越明确,测试越能暴露哪些依赖应该是参数、端口或边界适配器。
反馈、观察与状态覆盖
<Term def="测试结果对接口、依赖方向、错误表达和可观察副作用的反向信息,而不是单纯的通过或失败标签。">接口反馈</Term>要回答“测试为什么难写”。若夹具需要十个步骤、错误只能通过日志猜测,或每次测试都依赖系统时钟,先改善设计再增加更多案例。
<Term def="覆盖成功、边界、拒绝、重试、超时、权限和副作用状态的证据集合。">状态覆盖</Term>比单纯的行覆盖更接近行为风险。每个状态都要说明输入、预期结果、观察点和恢复方式;未覆盖状态要公开列出,不用绿色总数掩盖。
练习实验:先写行为,再听测试摩擦
Interactive lab
选择状态,观察测试如何反馈设计
输入证据
普通用户重复提交空购物车,拒绝响应应保持状态不变。
实际首差
边界节点:错误原因可见,未创建订单。
恢复动作
把拒绝场景加入端到端和局部状态测试。
先写用户可观察结果,再选择状态;失败时保存输入和首个差异,不要只把断言改得更宽。
1. 选择一条端到端行为
写出用户、请求、依赖和可见结果。先选择最小成功路径,再补一个拒绝路径;不从内部函数列表开始。
正常、边界与单一故障证据
| 样本 | 唯一变化 | 预期判定 | 必存证据 |
|---|---|---|---|
| 正常 | 已知请求和稳定依赖 | 用户路径完成,合同可重建 | 请求、响应和事件摘要 |
| 边界 | 空、越权、超时或重复请求 | 明确拒绝且状态不被污染 | 输入、错误和状态快照 |
| 单一故障 | 一个依赖、时钟或适配器失效 | 在首个差异处停止并恢复 | 失败输入、日志和回退 |
独立复核者应能从测试命令、种子、依赖版本和观测字段重放结论。端到端测试不是替代所有局部测试;局部测试负责快速、精确地保护规则,端到端测试负责证明真实边界仍然连通。
什么时候测试要求改变设计
当测试必须知道内部调用顺序、修改全局单例、等待任意睡眠时间或匹配不稳定日志时,优先修复可观察性和依赖边界。不要无限增加测试工具层来掩盖不可用接口。相反,当外部系统不可控时,可以在明确的适配器边界使用替身,并保留少量真实集成场景验证协议。
自动生成测试也要经过同一合同:生成器覆盖了哪些输入,断言证明什么,失败如何缩减,哪些外部副作用没有被触达。测试数量增长不能替代状态覆盖和可回退证据。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 端到端切片
从真实入口经过系统边界到可观察结果的最小行为路径。
- 测试视角
测试调用接口时所需的输入、身份、依赖、时钟和观察方式。
- 可测试性
调用者可以提供依赖、触发行为、读取结果并判断拒绝原因的结构特性。
- 接口反馈
测试结果对接口、依赖、错误表达和副作用设计的反向信息。
- 状态覆盖
覆盖成功、边界、拒绝、重试、超时、权限和副作用状态的证据集合。
练习
练习
问题 1: 一个测试需要先创建数据库、启动队列、等待两秒才能检查一个拒绝响应。你会把摩擦转成什么设计问题?
问题 2: 行覆盖率从 70% 提升到 95%,但重复提交仍生成两个订单。下一步做什么?
问题 3: 端到端测试失败,但单元测试全绿。你如何避免只改测试让流水线恢复?
本单元回顾
为编码测试,就是让测试成为第一个用户:从端到端行为开始,让测试摩擦暴露接口和依赖问题,再用局部测试缩短反馈、用状态覆盖保护边界。测试的结果不只是一种颜色,它还告诉我们哪里不可观察、哪里难以调用,以及失败后能否恢复。