20 调试
以复现、观测、假设和证伪驱动调试,先制造失败测试,再修首个因果分叉。
学习目标
- 能把一个失败拆成复现、数据、假设、实验和回归五个可复核节点
- 能修改实验中的一个条件,用错误信息和首差证伪一个调试假设
- 能回答:为什么修代码前必须先让测试稳定失败,并留下怎样的回归证据?
为什么这一单元不可压缩
线上故障常常只给你一条模糊的投诉、一段日志和一个“刚才还好好的”版本。如果直接打开编辑器猜补丁,问题也许暂时消失,却没人知道改动是否真的命中了原因。本单元把调试压缩成一条可重放的链:先保留失败,再收集证据,然后只改变一个条件,最后把修复变成下一次能自动拒绝旧错误的检查。
本页依据 David Thomas、Andrew Hunt《程序员修炼之道:通向务实的最高境界(第2版)》,云风译,电子工业出版社,2020年4月,ISBN 9787121384356 的公开中文目录,独立重构 20 调试 与 提示29—34。课程不复制原书正文、插图、练习答案或代码,而把目录命题转写成新的因果模型、故障样本、交互实验和复核证据。
先看问题:不要让“修好了”吞掉证据
把故障想成一条夜间巡检路线:你先在同一地点重新看到异常,再沿着路牌记录每个转弯,最后只关闭一个阀门确认水流是否改变。没有这条路线,修复就像换了一个零件后发现灯暂时亮了,下一位工程师无法判断应当信什么。
先预测:如果日志显示 select 成功、用户却拿到错误结果,哪一个节点应该先拒绝“数据库没有问题”的结论?打开下面的实验台,注入“删除最小复现”的单故障,再逐节点核对你的预测。
常见误区
20 调试:让问题先失败
调试的第一件事是建立 ↡在相同输入、版本和环境约束下,能够重复得到同一失败现象。 条件,而不是立即修改实现。可复现意味着别人能拿到同一份输入,看到同一条失败断言,并知道哪些条件没有被覆盖;它把“我这里有问题”变成可以交接的事实。
五个节点组成一条 ↡把失败样本、观测数据、解释、实验结果和防回归检查按先后关联起来的证据关系。:复现提供失败样本,数据提供错误信息,假设提出可检验解释,实验只改一个条件,回归测试记录修复后的边界。链条任何一处缺失,都只能说“结果变了”,不能说“原因已定位”。
提示29:去解决问题,而不是责备
把注意力从“谁造成了它”移到“哪个条件让它出现”。记录输入身份、代码版本、环境差异、期望结果、实际结果和复现频率;这些字段比一句“最近有人改过”更能帮助下一位工程师继续实验。若暂时找不到责任人,链条仍然可以推进;若没有失败样本,责备也不会生成诊断信息。
提示30:不要恐慌
恐慌会让人同时改配置、升级依赖、加日志和重启服务,最后无法判断哪一个变化带来了新结果。先把问题分成“已观察到的事实”和“尚未证明的解释”,保存原始错误信息,再决定下一步要隔离哪个条件。冷静不是放慢处理,而是让每次处理仍然可比较。
提示31:修代码前先让代码在测试中失败
先写出 ↡只保留触发失败所必需输入、依赖和断言的最小案例。,并确认它在修复前确实失败。这个失败测试是实验的对照组:如果它从未失败,之后的绿色可能来自测试没有跑到问题,或断言过于宽松。只有先看见红色,再看见同一输入变绿,修复才有可复核的方向。
提示32:读一下那些该死的出错信息
错误信息是观测数据,不是需要马上隐藏的噪声。保留错误类型、消息、堆栈、请求标识、时间窗口和相关输入,并区分“产生错误的节点”和“最后显示错误的节点”。日志中的 select 成功,只能说明某一次调用返回;它不能单独证明筛选条件、映射结果或用户看到的对象正确。
提示33:“select”没出问题
一个中间动作成功,不等于整个因果链成功。把 ↡对某个可能解释提出明确预测,并设计能让预测失败的一变量实验。 写成“若移除条件 X,结果 Y 应发生变化”;如果 Y 不变,假设就被拒绝。这样既不会把数据库、网络或调用者预先定罪,也不会被一个绿色的中间步骤误导。
提示34:不要假设,要证明
每轮实验都要预先写出期望、实际、唯一干预和拒绝条件。定位 ↡沿着因果链比较基线与故障样本时,第一个状态发生分叉的节点。 后,修复才有边界:它应让原始失败消失,同时不改变未参与实验的输入。最后用 ↡把已经修复过的失败固定成自动检查,防止同一类错误再次回归。 重放边界样本;若测试只检查“请求返回 200”,就还没有证明用户结果正确。
三步观察:从失败走到证伪
每一步都复用上面的因果图。动手前先写下你预计的首差;图中的高亮只表示教学位置,不替你证明真实系统已经满足条件。
1. 固定失败样本
记录输入身份、版本、环境、期望和实际输出,缩小到最小复现。先问“别人能否按这份记录重放”,再问“谁最后改过这里”。
运行实验:首差必须可见
猜一猜:删掉最小复现后,链路会在“实验”还是“假设”处先拒绝?下面的实验台只改变一个条件。先注入故障,再逐节点推进;如果失败只在最后才出现,说明中间的证据门槛没有被实现。
第 1 / 5 步:复现 已收到 失败样本。
决策账本:让另一位工程师能重放
| 字段 | 必答问题 | 拒绝条件 |
|---|---|---|
| 基线 | 哪个输入、版本、环境和期望结果被冻结? | 只有“线上偶发”描述,没有样本身份 |
| 干预 | 这轮实验只改变了哪个条件? | 同时换依赖、改配置、重写测试 |
| 首差 | 哪个节点先偏离预测,证据是什么? | 只引用最终堆栈或最后一条日志 |
| 修复 | 修复改变了哪个因果边界? | 只说“现在通过”,没有反例 |
| 回归 | 哪个测试会拒绝同一类旧错误? | 只测成功路径,不测原始边界 |
记录失败并不是悲观,而是给未来的复核者留下对照组。若某个假设被拒绝,就保留它和拒绝原因;删除错误尝试会让下一次排查重新支付同一笔成本。
本章回顾
- 先让失败稳定复现,再讨论责任与修复。
- 错误信息是证据,绿色的中间步骤不是结论。
- 一次实验只改变一个条件,并停在首差。
- 修复要重放原始输入、边界输入和相邻正常样本。
- 回归测试把一次定位变成团队以后可执行的约束。
完成本章后,你应能把一次“偶发 Bug”改写成别人可重放的失败样本,写出能被拒绝的假设,并用一条回归测试证明修复没有只是换了症状。
练习:把猜测改写成证据
练习
问题 1:最小复现。 一个订单查询在生产偶尔返回空列表。请写出至少五个应冻结的输入或环境字段,并说明哪个字段不应该靠口头描述替代。
问题 2:改 Demo 代码。 在实验台的“注入单故障”之外,再设计一个只改变一个条件的故障模式,例如把边界日期向前移动一天。请写出预测的首差、拒绝条件和重置后的初始状态。
问题 3:回归验收。 修复后,测试只断言 HTTP 状态为 200。请说明为什么这不足以证明 select 相关 Bug 已修复,并补写两条更有用的断言。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 可复现
在相同输入、版本和环境条件下,别人也能重新看到同一个失败。
- 证据链
把失败样本、观测数据、解释、实验结果和回归检查按顺序连起来的记录。
- 最小复现
触发问题所需的最少输入、依赖和断言,删掉多余部分后仍会失败。
- 可证伪假设
一个能写出明确预测,并且设计实验让它可能失败的解释。
- 首差
基线和故障样本比较时,第一个状态发生分叉的节点。
- 回归测试
把已经修复的失败固定成自动检查,防止同一类错误再次出现。
来源与改写范围
- 作者与英文版页面:核对 20 周年版书名、版本与 Topic 20 的目录范围。
- 中文公开目录:核对“20 调试”及提示29—34的目录坐标。
- MDN:JavaScript 错误参考:核对错误消息、堆栈与运行时诊断的技术事实边界。
- Node.js:断言 API:核对把预期结果固定为可执行断言的工具语义;本章模型、案例、图示和练习均为独立改写。