测试驱动开发(TDD)
概述
先写测试。看它失败。写最少的代码让它通过。
核心原则: 如果你没有看到测试失败,你就不知道它是否测试了正确的东西。
违反规则的字面意思就是违反规则的精神。
何时使用
始终使用:
- 新功能
- Bug 修复
- 重构
- 行为变更
例外(需询问你的人类伙伴):
- 一次性原型
- 生成的代码
- 配置文件
想着"就这一次跳过 TDD"?停下来。那是在给自己找借口。
铁律
先写了代码再写测试?删掉它。从头来过。
没有例外:
- 不要保留作为"参考"
- 不要在写测试时"改编"它
- 不要看它
- 删除就是删除
从测试出发,重新实现。句号。
红-绿-重构
红灯 - 编写失败的测试
写一个最小的测试来展示期望行为。
<Good>
名称清晰,测试真实行为,只测一件事
</Good>
<Bad>
名称模糊,测试的是 mock 而非代码
</Bad>
要求:
- 一个行为
- 清晰的名称
- 使用真实代码(除非不得已才用 mock)
验证红灯 - 看它失败
必须执行。绝不跳过。
确认:
- 测试失败(不是报错)
- 失败信息符合预期
- 失败原因是功能缺失(不是拼写错误)
测试通过了? 你在测试已有的行为。修改测试。
测试报错了? 修复错误,重新运行直到它正确地失败。
绿灯 - 最少代码
写最简单的代码让测试通过。
<Good>
刚好够通过测试
</Good>
<Bad>
过度设计
</Bad>
不要添加功能、重构其他代码或做超出测试要求的"改进"。
验证绿灯 - 看它通过
必须执行。
确认:
- 测试通过
- 其他测试仍然通过
- 输出干净(没有错误、警告)
测试失败了? 修改代码,不是测试。
其他测试失败了? 立即修复。
"其他测试"指的是项目的整个测试套件,不只是你那个文件。 你写的那个测试跑绿了,不等于测试套件是绿的。在宣称改动完成之前,运行项目的测试命令(直接 pytest、npm test、cargo test——仓库用什么就跑什么),即使你的任务只点名了一个测试文件。任务里的范围说明约束的是交付物,不是你的验证。那次运行暴露出的任何失败——包括不是你造成的——都要在报告里点名写出;一个你眼看着滚过去却没提的红色测试,就是一份靠遗漏造假的报告。
重构 - 清理代码
只有在绿灯之后才重构:
- 消除重复
- 改善命名
- 提取辅助函数
保持测试绿灯。不要添加行为。
重复
为下一个功能写下一个失败的测试。
好的测试
写任何测试、或修改任何测试时,阅读 writing-good-tests.md [blocked],那里是让测试保持诚实的规则:
- 在动手写之前,先点名那个会让该测试失败的生产代码改动
- 断言真实行为,绝不断言 mock 行为
- 只有测试才用的代码放在测试工具里,不进生产类
- 在 mock 一个依赖之前,先搞清它的副作用
常见借口
危险信号 - 停下来,从头开始
- 先写了代码再写测试
- 实现完了才补测试
- 测试立即通过
- 无法解释测试为什么失败
- "之后再补"测试
- 说服自己"就这一次"
- "我已经手动测试过了"
- "后补测试也能达到相同目的"
- "重要的是精神不是仪式"
- "留作参考"或"改编现有代码"
- "已经花了 X 小时了,删掉太浪费"
- "TDD 太教条了,我是在务实"
- "这次情况不同,因为……"
以上所有情况都意味着:删除代码。用 TDD 从头开始。
示例:Bug 修复
Bug: 空邮箱被接受了
红灯
验证红灯
绿灯
验证绿灯
重构 如果需要,提取验证逻辑以支持多个字段。
验证清单
在标记工作完成之前:
- 每个新函数/方法都有测试
- 在实现之前看到每个测试失败
- 每个测试因预期原因失败(功能缺失,不是拼写错误)
- 为每个测试编写了最少代码使其通过
- 所有测试通过
- 输出干净(没有错误、警告)
- 测试使用真实代码(只在不可避免时用 mock)
- 覆盖了边界情况和错误场景
不能全部勾选?你跳过了 TDD。从头开始。
遇到困难时
调试集成
发现 bug?写一个重现 bug 的失败测试。按 TDD 循环走。测试既证明了修复有效,又防止了回归。
绝不在没有测试的情况下修复 bug。
最终规则
没有你的人类伙伴的许可,没有例外。


