TDD 的核心是:先写测试,观察它失败,再写最小代码使其通过。若没有亲眼看到测试失败,就无法确认测试是否在检验正确的东西。违反规则的文字,就是违反规则的精神。
何时使用 始终适用于:新功能、缺陷修复、重构、行为变更。例外(需询问人类搭档):一次性原型、生成代码、配置文件。若想「就这一次跳过 TDD」,请停下,那是自我合理化。
铁律 没有失败的测试,不得编写生产代码。若先写了代码,删除它,重新开始。没有例外:不要保留为「参考」,不要在写测试时「改编」它,不要看它。删除就是删除。从测试开始全新实现。
红-绿-重构循环 流程为:RED 写失败测试 → 验证其正确失败 → GREEN 写最小代码 → 验证通过 → REFACTOR 清理 → 保持绿灯 → 下一个测试。
RED:写一个最小测试,展示应该发生什么。要求聚焦一个行为、名称清晰、使用真实代码(除非不可避免,否则不用 mock)。
验证 RED(强制,绝不跳过):运行测试,确认测试失败而非报错、失败信息符合预期、失败原因是功能缺失而非拼写错误。若测试通过,说明你在测试已有行为,需修正测试;若报错,修复错误后重跑直到正确失败。
GREEN:写最简单的代码让测试通过。不要添加功能、重构其他代码或超出测试范围「改进」。
验证 GREEN(强制):确认测试通过、其他测试仍通过、输出干净(无错误、警告)。若测试失败,修代码而非测试;若其他测试失败,立即修复。
REFACTOR:仅在绿灯后清理,包括消除重复、改进命名、提取辅助函数。保持测试绿灯,不添加行为。
重复:为下一个功能写下一个失败测试。
好测试的标准 最小:只做一件事,名称里出现「and」就拆分。清晰:名称描述行为,而非 test1。体现意图:展示期望的 API,而非掩盖代码应该做什么。
为什么顺序重要 「我之后写测试来验证它能工作」:事后测试会立即通过,立即通过什么也证明不了——可能测错东西、测实现而非行为、遗漏忘记的边界情况,你从未看到它抓到 bug。测试先行强制你看到失败,证明测试确实在检验东西。
「我已手动测过所有边界情况」:手动测试是临时的,没有测试记录,代码变更后无法重跑,压力下容易忘记用例。「我试的时候能用」不等于全面。自动化测试是系统的,每次运行方式一致。
「删除 X 小时工作很浪费」:沉没成本谬误,时间已经花掉。现在选择是:删除并用 TDD 重写(再花 X 小时,高置信度),或保留并事后加测试(30 分钟,低置信度,可能有 bug)。真正的浪费是保留你无法信任的代码。
「TDD 教条,务实应灵活」:TDD 就是务实的:提交前发现 bug、防止回归、记录行为、支持重构。所谓务实捷径等于在生产环境调试,更慢。
「事后测试达到同样目标,重精神而非仪式」:不对。事后测试回答「这做了什么?」,测试先行回答「这应该做什么?」。事后测试受实现偏见影响,验证的是你构建的东西而非需求。30 分钟事后测试不等于 TDD:你得到覆盖率,却失去测试有效的证明。
常见自我合理化 「太简单不用测」——简单代码也会坏,测试只要 30 秒。「我之后测」——立即通过证明不了什么。「已达同样目标」——事后测「做了什么」,先测「应做什么」。「已手动测过」——临时不等于系统,无记录、无法重跑。「删除 X 小时浪费」——沉没成本,保留未验证代码是技术债。「保留作参考,先写测试」——你会改编它,那就是事后测试。删除就是删除。「需要先探索」——可以,但丢弃探索结果,从 TDD 开始。「测试难写=设计不清」——听测试的,难测即难用。「TDD 会拖慢我」——TDD 比调试快。「手动更快」——手动不能证明边界,每次变更都要重测。「现有代码没测试」——你在改进它,为现有代码加测试。
红牌警示——停下并重新开始 测试前有代码;实现后才写测试;测试立即通过;无法解释测试为何失败;测试「之后」才加;合理化「就这一次」;「我已手动测过」;「事后测试达到同样目的」;「重精神不重仪式」;「保留作参考」或「改编现有代码」;「已花 X 小时,删除浪费」;「TDD 教条,我很务实」;「这次不同因为……」。这些全都意味着:删除代码,用 TDD 重新开始。
示例:缺陷修复 缺陷为接受空邮箱。RED 写测试:test('rejects empty email', 提交空邮箱应返回 'Email required')。验证 RED:npm test 失败,预期 'Email required' 却得到 undefined。GREEN:在 submitForm 中若邮箱为空则返回 { error: 'Email required' }。验证 GREEN:npm test 通过。REFACTOR:如需要,为多个字段提取校验逻辑。
验证清单(完成前) 每个新函数/方法都有测试;在实现前亲眼看到每个测试失败;每个测试因预期原因失败(功能缺失而非拼写错误);为每个测试写最小通过代码;所有测试通过;输出干净(无错误、警告);测试使用真实代码(仅在不可避免时用 mock);边界情况和错误已覆盖。无法全部勾选?你跳过了 TDD,重新开始。
卡住时 不知道怎么测:写期望的 API,先写断言,询问人类搭档。测试太复杂:设计太复杂,简化接口。必须 mock 一切:代码耦合过紧,用依赖注入。测试准备庞大:提取辅助函数;仍复杂则简化设计。
调试集成 发现 bug?写一个复现它的失败测试,遵循 TDD 循环。测试证明修复并防止回归。绝不在没有测试的情况下修 bug。
最终规则 生产代码 → 必须已存在且先失败过的测试。否则 → 不是 TDD。未经人类搭档许可,没有例外。