41 为编码测试

把测试当成代码的第一个用户和设计反馈源,从端到端切片推动接口、耦合与可观察性改善。

学习目标

  • 能把用户可观察行为写成端到端测试,并从测试摩擦发现接口和依赖问题
  • 能区分端到端、局部和性质证据的职责,不用行覆盖率代替行为合同
  • 能用失败样本、可观察日志和回退动作重放测试结论,让测试成为设计反馈

测试是代码的第一个用户

本页依据 David Thomas、Andrew Hunt《程序员修炼之道:通向务实的最高境界(第2版)》,云风译,电子工业出版社,2020 年 4 月,ISBN 9787121384356 的公开中文目录,独立重构 41 为编码测试。正文、代码、图示、实验和练习都是本课程重新设计的教学材料,不复制原书正文、插图或答案。

测试不只是发布前找 Bug 的筛子。它第一次调用接口、准备输入、观察结果并处理错误,因此测试写起来很费力,通常说明代码的依赖、边界或命名也很费力。把测试当成第一个用户,意味着先从真实行为开始,再让实现变得容易被调用、观察和恢复。

三个会让测试失去设计反馈的陷阱

从行为目标到可反馈实现

<Term def="从用户输入经过真实边界得到可观察结果、错误和副作用的最小行为路径。">端到端切片</Term>是本单元的起点。它不要求一次覆盖整个系统,而要求至少让一个真实行为从入口走到可验收结果。回路包含五个节点:

  1. 行为目标:说明谁在什么条件下期待什么结果。
  2. 测试接口:以调用者视角准备请求、身份、依赖和观察点。
  3. 最小实现:只实现让端到端场景前进一步的必要代码。
  4. 反馈:比较输出、错误、状态和副作用,定位首个差异。
  5. 扩展:增加边界、失败、权限和并发场景,再抽取局部测试。
测试是第一个用户:先证明行为,再扩展实现测试摩擦是设计反馈,端到端切片是最小可观察路径1行为目标谁要什么结果已留证据2测试接口以调用者准备当前接口入口3最小实现先连通一条路径等待证据4反馈比较行为差异等待证据5扩展覆盖边界与失败等待证据先从真实用户路径得到反馈,再用局部测试缩短迭代
专属图示:测试通过调用和观察结果,反向塑造可用的接口。

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 / 4

1. 选择一条端到端行为

写出用户、请求、依赖和可见结果。先选择最小成功路径,再补一个拒绝路径;不从内部函数列表开始。

正常、边界与单一故障证据

证据矩阵:状态覆盖不等于行覆盖率端到端证明边界连通,局部测试负责快速保护规则字段正常边界故障路径真实请求拒绝请求依赖失效观察响应与事件错误与状态首个差异反馈接口可用边界清楚回退动作扩展局部加速状态覆盖重放记录保存请求、身份、版本、日志、状态快照和回退路径,复核者才能重放
专属图示:正常、边界和故障行为共同定义测试的设计反馈。
样本唯一变化预期判定必存证据
正常已知请求和稳定依赖用户路径完成,合同可重建请求、响应和事件摘要
边界空、越权、超时或重复请求明确拒绝且状态不被污染输入、错误和状态快照
单一故障一个依赖、时钟或适配器失效在首个差异处停止并恢复失败输入、日志和回退

独立复核者应能从测试命令、种子、依赖版本和观测字段重放结论。端到端测试不是替代所有局部测试;局部测试负责快速、精确地保护规则,端到端测试负责证明真实边界仍然连通。

什么时候测试要求改变设计

当测试必须知道内部调用顺序、修改全局单例、等待任意睡眠时间或匹配不稳定日志时,优先修复可观察性和依赖边界。不要无限增加测试工具层来掩盖不可用接口。相反,当外部系统不可控时,可以在明确的适配器边界使用替身,并保留少量真实集成场景验证协议。

自动生成测试也要经过同一合同:生成器覆盖了哪些输入,断言证明什么,失败如何缩减,哪些外部副作用没有被触达。测试数量增长不能替代状态覆盖和可回退证据。

术语表

名词解释

本章出现的专业名词,用大白话再讲一遍。

端到端切片

从真实入口经过系统边界到可观察结果的最小行为路径。

测试视角

测试调用接口时所需的输入、身份、依赖、时钟和观察方式。

可测试性

调用者可以提供依赖、触发行为、读取结果并判断拒绝原因的结构特性。

接口反馈

测试结果对接口、依赖、错误表达和副作用设计的反向信息。

状态覆盖

覆盖成功、边界、拒绝、重试、超时、权限和副作用状态的证据集合。

练习

练习

问题 1: 一个测试需要先创建数据库、启动队列、等待两秒才能检查一个拒绝响应。你会把摩擦转成什么设计问题?

问题 2: 行覆盖率从 70% 提升到 95%,但重复提交仍生成两个订单。下一步做什么?

问题 3: 端到端测试失败,但单元测试全绿。你如何避免只改测试让流水线恢复?

资料与写作方式声明

本章以程序员修炼之道权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

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

本单元回顾

为编码测试,就是让测试成为第一个用户:从端到端行为开始,让测试摩擦暴露接口和依赖问题,再用局部测试缩短反馈、用状态覆盖保护边界。测试的结果不只是一种颜色,它还告诉我们哪里不可观察、哪里难以调用,以及失败后能否恢复。

前后导航

讨论

评论区加载中…