从失败测试追到根因,而不是罗列猜测
运行一个可复现的折扣计算 bug,建立执行链、对照实验和最小修复面。
先知道终点
- 做完你会得到
- 一份区分症状、根因、触发条件和修复证据的 diagnosis.md
- 开始前只需要
- 能在终端运行 node;理解只读诊断与实施修复是不同授权
- 最后留下这些证据
- 修复前测试稳定失败;根因指向具体代码路径;修复后回归测试通过
内容校准于 2026-07-30 · 第 23 / 38 节已发布课程
跟着材料做,不只阅读
本节练习资料
差一分钱,能猜出十个原因
折扣测试失败了。有人怀疑浮点数,有人怀疑缓存,还有人顺手把时区也列进来。可能性越来越完整,代码却一行没靠近根因。
诊断最容易伪装成工作量。
真正的交付物不是猜测清单,而是一条能被反驳的因果链。 这节课会运行一个失败的折扣计算器,把输入、执行路径、对照实验和最小修复放进同一份假设账本。
诊断的交付物是因果证据
“可能是缓存、网络或数据问题”不是诊断。合格诊断要回答:
- 症状怎样稳定复现;
- 请求或数据经过了哪些真实代码路径;
- 哪个对照实验排除了其他解释;
- 根因、触发条件和放大因素分别是什么;
- 最小修复为什么能阻断这条失败链。
本节使用可运行的折扣计算器,不靠假想案例。
第一步:建立修复前基线
下载两个 .mjs 文件放到同一空目录:
debugging-lab/
coupon-calculator.mjs
coupon-calculator.test.mjs
运行:
node --test coupon-calculator.test.mjs
预期:三项测试中有一项失败,“SAVE10”在 100 元订单上没有得到 90。把完整命令、退出码和失败断言保存到 diagnosis.md 的“症状”部分。
如果测试没有失败,先检查文件是否下载完整、名称是否一致、Node 版本是否支持 node:test。基线与教程不一致时不要继续,这正是成熟教程必须提供的失败分支。
第二步:先让 Codex 只读追执行链
目标
- 只读诊断 coupon-calculator.test.mjs 中的失败,不修改文件。
请按顺序
1. 运行失败测试并保留原始断言;
2. 从测试输入追到 calculateTotal 的每个判断;
3. 列出最多三个与当前证据一致的假设;
4. 为每个假设设计一个最小对照实验;
5. 区分症状、根因、触发条件和放大因素。
边界
- 不修改代码,不放宽断言,不增加依赖。
- 每个结论引用文件和具体表达式。
你应该看到什么
执行链应包含:测试构造订单 → 调用 calculateTotal → 标准化 coupon → 判断是否应用折扣 → 计算 total。若回答跳过中间值,只说“折扣逻辑有 bug”,要求它打印或推导具体变量。
第三步:手工做三个对照
在不改文件的前提下,让 Codex 或你自己运行:
node -e "import('./coupon-calculator.mjs').then(({calculateTotal}) => console.log(calculateTotal({subtotal:100,coupon:'SAVE10'})))"
node -e "import('./coupon-calculator.mjs').then(({calculateTotal}) => console.log(calculateTotal({subtotal:100,coupon:'save10'})))"
node -e "import('./coupon-calculator.mjs').then(({calculateTotal}) => console.log(calculateTotal({subtotal:100,coupon:' NONE '})))"
比较大写、已经小写和带空格无券三组。对照的目的不是收集更多输出,而是改变一个变量,观察失败是否随之变化。
如果小写 save10 生效而大写 SAVE10 不生效,问题就从“所有折扣计算错误”缩小到“输入标准化与比较约定不一致”。
第四步:写清四层因果
参考结构:
症状:SAVE10 用例期望 90,实际 100。
根因:coupon 被转换成小写后,却与大写常量 SAVE10 比较。
触发条件:调用方传入任意大小写形式的 SAVE10。
放大因素:当前只有一个失败用例,错误提示没有显示标准化后的值。
根因必须是能解释对照结果的具体机制。触发条件不是根因;“用户输入大写”本身没有错,除非契约明确只允许小写。
第五步:提出最小修复与反例
两个候选方案:
- 标准化为小写,并与 "save10" 比较;
- 不标准化,只接受 "SAVE10"。
检查现有测试与函数语义后,选择能保持大小写不敏感契约的方案。再问:它会不会误把 " SAVE10 "、空值或未知券码当成有效?
允许 Codex修改前,写清:
只修改 coupon-calculator.mjs 中券码比较,使现有大小写不敏感契约成立。
不要修改测试来迁就实现,不改变无券和未知券码行为。
修复后运行完整测试,并报告 diff。
第六步:验证修复没有只迎合一个样本
至少验证:
- 原失败测试从失败变为通过;
- 其他两项测试仍通过;
- 代码 diff 只包含必要比较变化;
- 对照命令的大写与小写结果一致;
- 未知券码仍返回原价。
把修复前后测试摘要都保留。只有“修复后绿灯”无法证明问题曾被正确复现。
第七步:对照参考诊断
打开答案文件,逐项比较:
- 是否有原始失败证据;
- 根因是否定位到同一个条件表达式;
- 是否把输入大写误写成用户错误;
- 修复是否改变了测试或其他行为;
- 是否说明未覆盖的边界。
常见失败
- Codex 直接修改:你没有把“只读诊断”作为任务边界。
- 它列出十个假设:要求每个假设必须对应当前证据和可执行对照。
- 它删除失败测试:这是把报警器关掉,不是修复。
- 它顺手重构函数:最小修复面不清楚,增加了回归范围。
让下一个实验淘汰一个解释
挑一个可复现 bug,只保留当前最能解释证据的两项假设。为每项写一个结果不同的对照实验。
实验没有区分力,就不值得运行。每轮诊断都应该让可能世界变少,而不是让报告变长。
完成检查
- 我保留了修复前的失败输出
- 执行链包含关键中间值和判断
- 对照实验一次只改变一个变量
- 根因、触发条件和放大因素已分开
- 修复后完整回归与 diff 都经过检查
参考与校准来源
本文是官方资料的中文转译与教学重组,不是逐字翻译;产品能力、命令、默认值与安全边界以下列官方原文为准。