20 调试

以复现、观测、假设和证伪驱动调试,先制造失败测试,再修首个因果分叉。

学习目标

  • 能把一个失败拆成复现、数据、假设、实验和回归五个可复核节点
  • 能修改实验中的一个条件,用错误信息和首差证伪一个调试假设
  • 能回答:为什么修代码前必须先让测试稳定失败,并留下怎样的回归证据?

为什么这一单元不可压缩

线上故障常常只给你一条模糊的投诉、一段日志和一个“刚才还好好的”版本。如果直接打开编辑器猜补丁,问题也许暂时消失,却没人知道改动是否真的命中了原因。本单元把调试压缩成一条可重放的链:先保留失败,再收集证据,然后只改变一个条件,最后把修复变成下一次能自动拒绝旧错误的检查。

本页依据 David Thomas、Andrew Hunt《程序员修炼之道:通向务实的最高境界(第2版)》,云风译,电子工业出版社,2020年4月,ISBN 9787121384356 的公开中文目录,独立重构 20 调试提示29—34。课程不复制原书正文、插图、练习答案或代码,而把目录命题转写成新的因果模型、故障样本、交互实验和复核证据。

先看问题:不要让“修好了”吞掉证据

把故障想成一条夜间巡检路线:你先在同一地点重新看到异常,再沿着路牌记录每个转弯,最后只关闭一个阀门确认水流是否改变。没有这条路线,修复就像换了一个零件后发现灯暂时亮了,下一位工程师无法判断应当信什么。

先预测:如果日志显示 select 成功、用户却拿到错误结果,哪一个节点应该先拒绝“数据库没有问题”的结论?打开下面的实验台,注入“删除最小复现”的单故障,再逐节点核对你的预测。

常见误区

20 调试:让问题先失败

调试的第一件事是建立 条件,而不是立即修改实现。可复现意味着别人能拿到同一份输入,看到同一条失败断言,并知道哪些条件没有被覆盖;它把“我这里有问题”变成可以交接的事实。

五个节点组成一条 :复现提供失败样本,数据提供错误信息,假设提出可检验解释,实验只改一个条件,回归测试记录修复后的边界。链条任何一处缺失,都只能说“结果变了”,不能说“原因已定位”。

20 调试:首差优先先固定失败样本,避免把人的情绪当成诊断证据1复现失败样本去解决问题,而不是责备交付:失败样本2数据错误信息不要恐慌交付:错误信息3假设最小复现修代码前先让代码在测试中失败交付:最小复现4实验首差读一下那些该死的出错信息交付:首差5回归回归测试不要假设,要证明交付:回归测试验收合同:复现、数据、假设、实验、回归都能由同一输入重建每一步只改变一个条件,并保存首差、拒绝原因和回归测试先预测首差,再决定下一次实验;不要用最终结果替代中间证据
调试的最短路径不是猜补丁,而是让证据在第一个分叉处拒绝错误假设。

提示29:去解决问题,而不是责备

把注意力从“谁造成了它”移到“哪个条件让它出现”。记录输入身份、代码版本、环境差异、期望结果、实际结果和复现频率;这些字段比一句“最近有人改过”更能帮助下一位工程师继续实验。若暂时找不到责任人,链条仍然可以推进;若没有失败样本,责备也不会生成诊断信息。

提示30:不要恐慌

恐慌会让人同时改配置、升级依赖、加日志和重启服务,最后无法判断哪一个变化带来了新结果。先把问题分成“已观察到的事实”和“尚未证明的解释”,保存原始错误信息,再决定下一步要隔离哪个条件。冷静不是放慢处理,而是让每次处理仍然可比较。

提示31:修代码前先让代码在测试中失败

先写出 ,并确认它在修复前确实失败。这个失败测试是实验的对照组:如果它从未失败,之后的绿色可能来自测试没有跑到问题,或断言过于宽松。只有先看见红色,再看见同一输入变绿,修复才有可复核的方向。

提示32:读一下那些该死的出错信息

错误信息是观测数据,不是需要马上隐藏的噪声。保留错误类型、消息、堆栈、请求标识、时间窗口和相关输入,并区分“产生错误的节点”和“最后显示错误的节点”。日志中的 select 成功,只能说明某一次调用返回;它不能单独证明筛选条件、映射结果或用户看到的对象正确。

提示33:“select”没出问题

一个中间动作成功,不等于整个因果链成功。把 写成“若移除条件 X,结果 Y 应发生变化”;如果 Y 不变,假设就被拒绝。这样既不会把数据库、网络或调用者预先定罪,也不会被一个绿色的中间步骤误导。

提示34:不要假设,要证明

每轮实验都要预先写出期望、实际、唯一干预和拒绝条件。定位 后,修复才有边界:它应让原始失败消失,同时不改变未参与实验的输入。最后用 重放边界样本;若测试只检查“请求返回 200”,就还没有证明用户结果正确。

三步观察:从失败走到证伪

每一步都复用上面的因果图。动手前先写下你预计的首差;图中的高亮只表示教学位置,不替你证明真实系统已经满足条件。

分步1 / 3

1. 固定失败样本

记录输入身份、版本、环境、期望和实际输出,缩小到最小复现。先问“别人能否按这份记录重放”,再问“谁最后改过这里”。

20 调试:首差优先先固定失败样本,避免把人的情绪当成诊断证据1复现失败样本去解决问题,而不是责备交付:失败样本2数据错误信息不要恐慌交付:错误信息3假设最小复现修代码前先让代码在测试中失败交付:最小复现4实验首差读一下那些该死的出错信息交付:首差5回归回归测试不要假设,要证明交付:回归测试验收合同:复现、数据、假设、实验、回归都能由同一输入重建每一步只改变一个条件,并保存首差、拒绝原因和回归测试先预测首差,再决定下一次实验;不要用最终结果替代中间证据
调试的最短路径不是猜补丁,而是让证据在第一个分叉处拒绝错误假设。

运行实验:首差必须可见

猜一猜:删掉最小复现后,链路会在“实验”还是“假设”处先拒绝?下面的实验台只改变一个条件。先注入故障,再逐节点推进;如果失败只在最后才出现,说明中间的证据门槛没有被实现。

提示31 / 34 · 首差实验台
一次调试如何从失败样本走到回归测试当前:第 1 步 · 复现 · 失败样本1复现失败样本去解决问题,而不是责备交付:失败样本2数据错误信息不要恐慌交付:错误信息3假设最小复现修代码前先让代码在测试中失败交付:最小复现4实验首差读一下那些该死的出错信息交付:首差5回归回归测试不要假设,要证明交付:回归测试验收合同:复现、数据、假设、实验、回归都能由同一输入重建每一步只改变一个条件,并保存首差、拒绝原因和回归测试

第 1 / 5 步:复现 已收到 失败样本。

先注入一个故障并预测首差,再逐节点推进;重置后应回到完整失败样本。

决策账本:让另一位工程师能重放

字段必答问题拒绝条件
基线哪个输入、版本、环境和期望结果被冻结?只有“线上偶发”描述,没有样本身份
干预这轮实验只改变了哪个条件?同时换依赖、改配置、重写测试
首差哪个节点先偏离预测,证据是什么?只引用最终堆栈或最后一条日志
修复修复改变了哪个因果边界?只说“现在通过”,没有反例
回归哪个测试会拒绝同一类旧错误?只测成功路径,不测原始边界

记录失败并不是悲观,而是给未来的复核者留下对照组。若某个假设被拒绝,就保留它和拒绝原因;删除错误尝试会让下一次排查重新支付同一笔成本。

本章回顾

  • 先让失败稳定复现,再讨论责任与修复。
  • 错误信息是证据,绿色的中间步骤不是结论。
  • 一次实验只改变一个条件,并停在首差。
  • 修复要重放原始输入、边界输入和相邻正常样本。
  • 回归测试把一次定位变成团队以后可执行的约束。

完成本章后,你应能把一次“偶发 Bug”改写成别人可重放的失败样本,写出能被拒绝的假设,并用一条回归测试证明修复没有只是换了症状。

练习:把猜测改写成证据

练习

问题 1:最小复现。 一个订单查询在生产偶尔返回空列表。请写出至少五个应冻结的输入或环境字段,并说明哪个字段不应该靠口头描述替代。

问题 2:改 Demo 代码。 在实验台的“注入单故障”之外,再设计一个只改变一个条件的故障模式,例如把边界日期向前移动一天。请写出预测的首差、拒绝条件和重置后的初始状态。

问题 3:回归验收。 修复后,测试只断言 HTTP 状态为 200。请说明为什么这不足以证明 select 相关 Bug 已修复,并补写两条更有用的断言。

名词解释

名词解释

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

可复现

在相同输入、版本和环境条件下,别人也能重新看到同一个失败。

证据链

把失败样本、观测数据、解释、实验结果和回归检查按顺序连起来的记录。

最小复现

触发问题所需的最少输入、依赖和断言,删掉多余部分后仍会失败。

可证伪假设

一个能写出明确预测,并且设计实验让它可能失败的解释。

首差

基线和故障样本比较时,第一个状态发生分叉的节点。

回归测试

把已经修复的失败固定成自动检查,防止同一类错误再次出现。

来源与改写范围

前后导航

讨论

评论区加载中…