没有失败的测试就不能写生产代码,因为只有亲眼看到测试失败,才能证明这个测试在测真实的功能缺失,而不是一个永远为真的假设或错误的断言。本文将基于 SuperPowers 的 TDD 技能源码,拆解这条铁律背后的逻辑,演示红绿重构的完整循环,并分析开发者与 AI 代理最容易陷入的测试反模式。
SuperPowers TDD 技能:为什么没有失败测试就不能写生产代码
在 AI 辅助编程的上下文中,测试驱动开发(TDD)的价值不仅在于“有测试”,更在于执行的顺序。SuperPowers 的 test-driven-development 技能将此固化为一条铁律:没有失败的测试,就不能写生产代码。这并非教条,而是一个验证机制——它确保你所写的测试能够真正捕捉到未来的回归问题。本指南将基于该技能的源文件,详细解释其工作原理与强制执行流程。
TDD 的核心:验证测试的有效性
TDD 解决的根本问题是:如何确信你的安全网真的能接住你?一个从未失败过的测试无法证明它测对了东西。它可能断言条件写反了,可能测的是一个 mock 的行为,也可能测的功能早已存在。只有看到它因为“功能缺失”而明确失败,才能证明这个测试是有效的,从而信任它未来的通过能告诉你代码是正确的。
铁律:没有失败的测试,就不能写生产代码
这是 SuperPowers TDD 技能的基石。源文件明确指出:
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Write code before the test? Delete it. Start over. No exceptions: Don’t keep it as “reference”, Don’t “adapt” it while writing tests, Don’t look at it, Delete means delete.
违反字面规则就是违反精神。一旦接受“可以先写代码再补测试”,后续所有的质量保障都会打折扣。技能文件甚至列举了常见的“合理化”借口(如“太简单了不用测”、“删掉几小时的工作太浪费”),并一一驳斥。
红绿重构:AI 代理的可执行循环
SuperPowers 将 TDD 的操作单元定义为 RED-GREEN-REFACTOR 循环,每个步骤都包含强制验证。
RED:写一个会失败的测试
目标是为一小段新行为编写一个最小化的测试。
- 要求:只测一件事,名字清晰描述行为,尽量测真实代码而非 mock。
- 输出:一个可以运行的测试文件。
Verify RED:亲眼看到它失败(强制步骤)
运行测试,如 npm test path/to/test.test.ts。你必须确认:
- 测试失败(断言失败,不是语法错误)。
- 失败原因是因为你期望的功能还不存在。
- 如果测试立刻通过了,说明你在测已存在的行为或测试写错了,必须回头修改测试。
为什么这一步不可或缺? 这是 TDD 与“事后补测试”的本质区别。一个立刻通过的测试,只能证明“现有代码做了什么”,无法证明“代码是否做了正确的事”。只有看到它因功能缺失而失败,你才能确信这个测试在守护正确的行为。
GREEN:写最少的代码让测试通过
目标是通过测试,而不是实现完美的功能。只写刚好能让测试通过的代码,避免过度设计(YAGNI)。
Verify GREEN:确认全部测试通过
再次运行完整的测试套件。确保:
- 新写的测试通过了。
- 所有旧测试依然通过。 新代码不能破坏已有行为。
REFACTOR:在绿灯下整理代码
所有测试通过后,可以清理代码结构(去重、改善命名、提取函数)。重构过程中必须保持测试全绿。重构不添加新行为,新行为需要新的测试循环。
警惕五类测试反模式
SuperPowers 的 testing-anti-patterns.md 文件专门列出了常见陷阱,遵循 TDD 流程能有效避免它们。
- 测试 mock 的行为而非真实行为:你验证的是
sidebar-mock是否存在,而不是页面是否正确渲染了导航栏。修正方法是不 mock,或 mock 后断言组件的真实行为。 - 在生产类中添加仅测试用的方法:例如在
Session类里添加一个只在测试的afterEach中调用的destroy()方法。这会污染生产代码。应将测试清理逻辑放在test-utils/工具文件中。 - 不理解依赖就开始 mock:随便 mock 掉一个有副作用的方法,而这个副作用恰恰是测试所需要的(如写入配置),导致测试通过但原因错误。mock 前必须理解依赖链,只在正确层级 mock 慢或外部的依赖。
- mock 数据结构不完整:只 mock 了你当前关心的字段,但下游代码依赖了其他字段。这会制造虚假的安全感。mock 时应完整镜像真实 API 的返回结构。
- 把测试当作收尾动作:“实现完了,接下来写测试”——这等于把测试降格为可选步骤。TDD 强制测试是开发流程的起点,而非终点。
TDD 原则也用于验证技能文档
SuperPowers 的一个亮点是,它将 TDD 思想应用于其自身技能文档的测试(参见 testing-skills-with-subagents.md)。测试一个强制 TDD 的技能,就像应用 TDD 到流程文档:
- RED:让 AI 代理在没有技能文档的情况下处理高压场景(如“写了 4 小时代码忘了测试,该下班了”),观察它如何合理化跳过 TDD。
- GREEN:编写技能文档,明确禁止这些合理化借口。
- REFACTOR:当代理在压力下仍找到新借口时,更新技能文档以堵塞漏洞。 这个过程确保了技能本身是“防弹”的,能够有效约束代理行为。
验证清单:你真的做到 TDD 了吗?
完成一个功能点后,用这个清单自检:
- [ ] 每个新函数/方法都有对应测试。
- [ ] 亲眼看到每个测试在实现前失败。
- [ ] 每个测试都因“功能缺失”而失败。
- [ ] 写了最少代码使测试通过。
- [ ] 所有测试(包括旧测试)通过。
- [ ] 测试测的是真实行为,而非 mock 行为。
- [ ] 覆盖了边界条件和错误路径。
任何一项打勾失败,都意味着跳过了 TDD 流程,需要回溯修正。这能确保你的 AI 编码代理产出的代码不仅功能正确,而且具备长期可维护的测试保障。你可以从 SuperPowers 安装指南 开始,在 Claude Code、Codex 等平台中集成并实践这一流程。
FAQ
Q: 每次都先写测试再写代码,开发速度会不会变慢? A: 短期看,单独写测试确实增加了编码前的思考时间。但长期看,它极大地减少了后期的调试、回归和返工时间。更重要的是,测试先行迫使你在实现前就想清楚 API 设计和边界条件,这往往能提升代码的初始设计质量,从根源上减少复杂度。
Q: 对于一个已经有很多未测试代码的老项目,加入新功能时还需要严格遵守 TDD 吗? A: 需要。新功能本身必须走 TDD 流程。对于新功能所依赖的、没有测试的旧代码,可以视情况补充一些关键路径的测试,但不必一次性补齐所有历史债务。核心原则是:不能用“旧代码没测试”作为新代码不写测试的理由。
Q: 写测试时感觉很难写,不知道测什么,是设计有问题吗? A: 很可能是。测试难写通常是代码设计问题的早期信号,比如函数职责过多、模块间耦合过紧、依赖注入不足等。把“测试难写”当作一个改进设计的提示,比在生产环境中发现设计缺陷要廉价得多。
Q: 修复一个 Bug,也需要先写一个能复现它的失败测试吗? A: 是的,这是 TDD 中 Bug 修复的标准做法。先写一个能稳定复现 Bug 的失败测试,然后再修复代码让测试通过。这样,你的修复有了明确的证据,而且这个测试将成为一个永久的“哨兵”,防止同类问题再次出现。