测试驱动开发
铁律:在没有失败测试的前提下,不得编写任何生产代码。
为什么 TDD 不可妥协
测试驱动开发(TDD)是软件工程中争议最多、也最常被跳过的实践。开发者会说先写测试太慢、对某些代码来说很别扭、截止日期太近了、或者说以后再补测试。
Superpowers 将所有这些论点视为自我合理化,没有一条是跳过测试优先纪律的可接受理由。
以下是先写代码的核心问题:当你先写代码再写测试时,测试是为了匹配代码而写的,而不是为了验证意图。 你最终测试的是代码做了什么,而不是它应该做什么。测试变成了文档制品,而不是正确性保证。原始代码中存在的 bug 被编码进测试,成为"预期行为"。
当你先写测试时:
- 你被迫在决定如何实现之前,先明确你想要的确切行为
- 你在构建实现之前就发现了接口问题(此时修复很便宜)
- 失败的测试证明你的测试确实在测试某些东西
- 实现后通过的测试证明你构建了你所意图的东西
TDD 并不慢。它消除的是缓慢的部分:调试从未被正确规格化的代码所花费的时间。
RED-GREEN-REFACTOR 循环
TDD 对每一个功能点都遵循简单的三阶段循环:
┌─────────────────────────────────────────────────────────────┐
│ │
│ RED ──────────────────→ GREEN ──────────────→ REFACTOR │
│ │
│ 为所需行为编写 用尽可能简单的代 在不改变行为 │
│ 一个失败测试。 码使测试通过。 的前提下改善 │
│ 代码。 │
└─────────────────────────────────────────────────────────────┘
RED:编写失败测试
编写一个测试,精确描述一个所需行为。运行它。看着它失败。
这一步不可省略。 如果你写了一个测试但它在没有任何实现的情况下立即通过,那么以下两种情况之一是真的:
- 功能已经存在(你应该检查原因)
- 你的测试是错误的——它并没有真正测试你以为它在测试的东西
在实现存在之前就通过的测试毫无价值。red 阶段的存在是为了确认你的测试是真实的。
// RED:这个测试失败,因为 calculateDiscount 还不存在
test('applies 10% discount for orders over $100', () => {
const order = { total: 150, items: [] };
expect(calculateDiscount(order)).toBe(15);
});
GREEN:使测试通过(用最简单的方式)
编写使失败测试通过的最简单代码。不是最优雅的代码。不是最可扩展的代码。而是最简单的代码。
这条规则防止在实现阶段过度设计。你不被允许为尚未有测试的情况构建代码。如果你想"那 $500 以上订单 20% 折扣怎么办?"——先为那个情况写测试,再使其通过。
// GREEN:使测试通过的最简单实现
function calculateDiscount(order: Order): number {
if (order.total > 100) {
return order.total * 0.10;
}
return 0;
}
REFACTOR:在不破坏的前提下改善
一旦测试通过,你就有了安全网。现在你可以改善代码:更好的命名、提取辅助函数、消除重复、提升可读性——任何能提升质量而不改变行为的事情。
每次重构后运行测试。如果测试仍然通过,你没有破坏任何东西。如果测试失败,撤销上一次修改。
// REFACTOR:更清晰的命名和常量
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.10;
function calculateDiscount(order: Order): number {
const qualifiesForDiscount = order.total > DISCOUNT_THRESHOLD;
return qualifiesForDiscount ? order.total * DISCOUNT_RATE : 0;
}
测试仍然通过。代码更清晰。循环完成。
代码在测试之前编写时该怎么做
这种情况会发生。AI agent 会在测试之前编写实现代码。处于截止日期压力下的开发者会这样做。问题是当它发生时该怎么办。
Superpowers 的立场很明确:
删除它。重新开始。
不是"现在补测试"。不是"差不多就行了"。删除实现代码,先写测试。
这看起来很苛刻,直到你理解原因。在没有失败测试的情况下编写的代码没有经过验证的规格。实现编码了未经挑战的假设。当你事后写测试时,你在写一个你知道会通过的测试——这意味着你没有发现任何东西。你只是在检查你的代码是否做了你的代码所做的事情。
测试优先纪律不仅仅是关于拥有测试。它是关于在实现之前规格化行为的过程。事后补充的测试跳过了这一步。
如果:生产代码在没有前置失败测试的情况下被编写
那么:
1. 删除生产代码
2. 提交删除操作
3. 编写一个失败测试
4. 针对测试进行实现
5. 继续
合理化借口对照表
当跳过 TDD 的压力累积时,以下是常见的论点以及它们为何站不住脚:
| 借口 | 现实 |
|---|---|
| "我以后会补测试的" | 事后添加的测试测试的是代码做了什么,而不是它应该做什么。"以后"很少真的到来。 |
| "这段代码太简单了,不需要测试" | 简单的代码也有 bug。测试只需要 2 分钟。 |
| "明天就是截止日期了" | 快速交付未经测试的代码产生的 bug,修复时间比写测试的时间还长。 |
| "我无法为这种代码写测试" | 你可以。如果真的很难测试,那是代码设计有问题的信号。 |
| "我们先做探索性编码,再正式化" | 探索性代码会变成生产代码。删除它或测试它——没有第三个选项。 |
| "是 AI 生成的,应该没问题" | AI 生成的代码有 bug。没有测试的 AI 生成代码有未被发现的 bug。 |
| "现在添加测试会让我们减速" | 在生产环境中发现 bug 之后再添加测试要慢得多。 |
| "测试这个需要 mock 太多东西了" | 太多 mock 意味着设计有太多依赖。修复设计。 |
完成前的验证
在声明任何功能完成之前,Superpowers 验证协议要求一个 5 步关卡:
┌────────────────────────────────────────────────────────────────┐
│ 验证关卡 │
│ │
│ 第 1 步:IDENTIFY — 列出与此功能相关的每个测试 │
│ 第 2 步:RUN — 执行测试套件(不要只是假设) │
│ 第 3 步:READ — 逐行阅读实际输出 │
│ 第 4 步:VERIFY — 在输出中确认每个测试都通过了 │
│ 第 5 步:CLAIM — 只有到这时才能声明功能已完成 │
└────────────────────────────────────────────────────────────────┘
第 1 步:IDENTIFY
列出每个涵盖正在完成的功能的测试文件和测试用例。不只是你今天写的测试——还包括任何触及受影响代码的现有测试。
第 2 步:RUN
实际运行测试套件。不是在脑子里。不是假设。运行它。
npm test -- --coverage --testPathPattern=order
第 3 步:READ
阅读实际输出。每一行。不只是"整体是通过还是失败"。查看:
- 运行了哪些测试
- 哪些测试通过了
- 哪些测试被跳过了
- 覆盖率数字
第 4 步:VERIFY
对于你的 IDENTIFY 列表中的每个测试,确认它出现在输出中并显示为通过。如果某个测试在输出中缺失,它没有运行。如果某个测试失败,功能没有完成。
第 5 步:CLAIM
只有在完成第 1–4 步之后,AI(或开发者)才可以声明:"这个功能已完成。"
说"它应该能用"不是完成第 5 步。 只有实际的测试输出才能完成第 5 步。
需要停止工作的红色警报
如果在 TDD 过程中出现以下任何情况,立即停止工作并升级:
| 红色警报 | 含义 |
|---|---|
| 测试在实现存在之前就通过了 | 测试可能是错误的 |
| 测试通过但功能行为不正确 | 测试测试了错误的东西 |
| 大型重构后第一次运行就全部通过 | 可疑——验证没有测试被无意中跳过 |
| 添加新功能时测试覆盖率下降 | 正在引入空白 |
| 一个测试需要修改 15 个其他测试才能通过 | 设计变更太大;将其分解为更小的步骤 |
| AI 声称测试通过但没有显示输出 | 不可接受——在继续之前要求实际输出 |
TDD 在 Superpowers 计划中的应用
如撰写计划中所述,计划中的每项实现任务之前必须有一项测试任务。计划结构在计划层面强制执行 TDD:
任务 N: 为 [行为] 编写失败测试 ← RED
任务 N+1: 实现 [行为] ← GREEN
[重构发生在任务 N+1 内,或如果范围需要则作为任务 N+2]
在执行计划时,如果 subagent 被分派到任务 N+1(实现)而任务 N(测试)尚未完成,subagent 必须拒绝该任务并将其报告为 BLOCKED。没有前置失败测试的实现任务是计划缺陷。
使用 Superpowers 运行 TDD
要显式调用 TDD 技能:
/test-driven-development I need to implement user authentication
AI 将引导你完成 RED-GREEN-REFACTOR 循环,强制执行失败测试优先要求,并在标记任何事情为完成之前应用 5 步验证关卡。
TDD 是相信你的代码能用和知道你的代码能用之间的区别。 在专业软件开发中,相信不是可接受的标准。证据才是。