该技能规定核心原则:先写测试,看着它失败,再写最小代码让它通过;如果没看到测试失败,就无法确认测试检验的是正确的东西。违反规则字面即违反规则精神。
适用时机:新功能、缺陷修复、重构、行为变更均应使用;一次性原型、生成代码、配置文件属于例外,需先询问人类搭档。若想“这次先跳过 TDD”,应停止,这属于自我合理化。
铁律:没有先失败的测试,就不写生产代码。若先写了代码,删除并重来——不保留作“参考”、不在写测试时“改造”它、不看它;删除就是删除,从测试重新实现。
红-绿-重构循环:
- RED:写一个最小测试,描述期望行为。测试应只有一个行为、名称清晰、使用真实代码(除非不可避免才用 mock)。不要写模糊名称或只测 mock 的测试。
- 验证 RED(强制):运行测试(如 npm test path/to/test.test.ts),确认测试失败而非报错、失败信息符合预期、失败原因是功能缺失而非拼写错误。若测试直接通过,说明测的是已有行为,应修正测试;若报错,先修错误再重跑直到正确失败。
- GREEN:写通过测试的最简代码,不额外加功能、不重构其他代码、不做超出测试的“改进”。
- 验证 GREEN(强制):确认该测试通过、其他测试仍通过、输出干净无错误警告。测试失败就改代码不改测试;其他测试失败立即修复。这里的“其他测试”指项目整套测试,不只自己那个文件:完成任务前要运行项目所用测试命令(如 pytest、npm test、cargo test),即使任务只点名一个测试文件;任何失败(包括非自己造成的)都要在报告中点名,看到红测试却未提及即属报告失实。
- REFACTOR:仅在测试通过后清理——去重、改进命名、提取辅助函数,保持测试通过,不添加行为。然后进入下一个失败测试。
好测试标准:最小(只测一件事,“and”出现就拆分)、清晰(名称描述行为,而非 test1)、体现意图(展示期望 API,而非掩盖代码应做什么)。写作或修改测试时需阅读 writing-good-tests.md,规则包括:写测试前先说出会让它失败的生产代码改动;断言真实行为而非 mock 行为;测试专用代码留在测试工具中,不进入生产类;mock 依赖前先理解其副作用。
常见借口与反驳:如“太简单不用测”(简单代码也会坏,测试只要 30 秒)、“之后补测试”(后写测试会立即通过,证明不了什么,且受已写代码偏见影响)、“已手动测过”(手动测试无记录、难复跑、压力下易漏)、“删掉几小时工作浪费”(沉没成本谬误,真正浪费的是保留不可信代码)、“留作参考再先写测试”(会改造它,等于后写测试)、“需要先探索”(可以探索后丢弃再 TDD)、“难测说明设计不清”(难测即难用)、“TDD 拖慢进度”(TDD 才是务实路径,能提交前抓 bug、防回归、放心重构)、“手动更快”(手动不证明边界情况)、“现有代码没测试”(正好为其补测试)。
危险信号(看到即停止重来):测试前写代码、实现后补测试、测试立即通过、说不清测试为何失败、事后加测试、“就这一次”合理化、已手动测过、后写测试目的相同、重精神不重仪式、留作参考或改造现有代码、已花数小时删了可惜、TDD 教条我务实、这次不一样。
验证清单:每个新函数/方法都有测试;实现前看过每个测试失败;每个测试因预期原因失败;写了最小通过代码;所有测试通过;输出干净;测试使用真实代码(mock 仅在不可避免时);覆盖边界与错误。无法全部勾选即说明跳过了 TDD,应重来。
卡住时:不知怎么测,就写出期望 API、先写断言或问人类搭档;测试太复杂说明设计太复杂,应简化接口;什么都要 mock 说明耦合过重,应用依赖注入;测试准备过多,先提取辅助函数,仍复杂则简化设计。
调试集成:发现 bug 时,写复现它的失败测试,走 TDD 循环;测试既证明修复又防回归。没有测试绝不修 bug。
最终规则:生产代码必须有先失败的测试;否则就不是 TDD。未经人类搭档许可无例外。