系列目录:Superpowers 教程系列索引
一、决策上下文
你跟 Claude 说"用 TDD 帮我实现这个功能"。它确实给你写了测试,也写了实现,最后还跑了一遍说全绿。看起来一切正常。
但你点开 git 历史会发现:测试和实现是同一次 commit 进来的,时间戳间隔几秒。它没经历过 RED。它先在脑子里写完了实现,然后倒推一份能通过的测试贴上。
这不是 AI 不懂 TDD。它知道 RED-GREEN-REFACTOR 的定义,能背书,能给你画完整流程图。它不愿意按这个顺序工作。原因也好理解:它的训练目标是"快速产出可工作的代码",TDD 要求的"先看一次失败"在 token 预测视角下是一笔无意义的浪费。
Superpowers 的 test-driven-development skill 解决的就是这件事。它不是在教 AI 什么是 TDD,是在阻止 AI 跳过 RED 阶段。
二、方案 A:让 AI 自由发挥的 TDD
我先描述不用 skill 时的实际行为。你给 Claude 的 prompt 是"用 TDD 实现一个标签验证类,名字不能为空、长度 1-50、只允许字母数字连字符"。
它的输出大概率长这样:
def test_validation():
assert Tag("python").name == "python"
with pytest.raises(ValidationError):
Tag("")
with pytest.raises(ValidationError):
Tag("a" * 51)
with pytest.raises(ValidationError):
Tag("test@bad")
class Tag:
def __init__(self, name):
if not name or len(name) > 50 or not re.match(r'^[\w\s-]+$', name):
raise ValidationError(...)
self.name = name
测试和实现一次性给你。表面上这是 TDD 的产出。实际上它做的是反过来的过程:先在脑子里把实现想清楚,再写一组刚好覆盖这个实现的测试。我把这种叫"事后 TDD"。
事后 TDD 的问题不是结果不对,是它丢了 TDD 真正的价值。TDD 的设计反馈来自"先写测试时发现 API 难用",你写到一半才意识到入参奇怪、依赖复杂、副作用难 mock,于是回头改设计。AI 不经历这一步,所以它不会替你发现"name 这个字段叫 raw_name 更合适"或者"应该返回结果对象而不是抛异常"这种设计层面的反馈。它给你的是一个看着像 TDD 产出、本质是后端开发的代码。
我最早怀疑这件事时做过一个粗糙的实验:让 Claude 用 TDD 实现某功能,然后让另一个会话只看到测试代码,反推"按这组测试推断要实现的功能边界"。反推出来的功能描述比我原始需求要窄。事后 TDD 的测试是实现的影子,不是需求的影子。
三、方案 B:Superpowers 的强约束 TDD
Superpowers 的 test-driven-development skill 的 SKILL.md 里有几条像看门狗的硬规则:
先写测试。测试失败后才能写实现。只写刚好通过测试的代码。通过后必须重构。
这几条单独看是 TDD 的常识。关键在于它们被写进了一个 Claude 在每次开始写代码前必读的文件,通过 progressive disclosure 机制激活(详见上一篇 Superpowers Skills 拆解)。
这意味着实际执行流程变成了:
flowchart LR
A[用户提需求] --> B[加载 TDD skill]
B --> C[写第一个测试]
C --> D[运行测试看到 FAILED]
D --> E[FAILED 现在是日志里的事件]
E --> F[写最少代码通过]
F --> G[运行测试看到 PASSED]
G --> H[决定要不要重构]
E 这一步是关键。在自由 TDD 里,“测试失败"是 AI 脑内的一个概念。在强约束下,“测试失败"是终端里实际跑出来的 FAILED 输出,是会话里能引用的真实事件。AI 不能跳过它,因为下一步实现要引用这次失败作为前置条件。
这个差别看着小,落地效果完全不同。
四、对比维度
| 维度 | 自由 TDD | Superpowers 强约束 TDD |
|---|---|---|
| RED 阶段 | 跳过(脑内进行) | 必须运行测试看到 FAILED |
| 测试覆盖思路 | 围绕已有实现倒推 | 从需求边界正向展开 |
| 设计反馈 | 几乎没有 | API 难写时会被迫修需求 |
| 重构步骤 | 经常省略 | 是独立一个阶段 |
| 失败模式 | 结果对但丢了过程 | 慢但纪律性可观察 |
我最初以为事后 TDD 只是顺序问题,跑得快慢不同。对比下来发现不是。丢的是设计反馈和边界覆盖,不是速度。
测试覆盖思路这一行最值得展开。事后 TDD 写的测试覆盖率可能很高,但测试边界跟实现的边界一样宽。实现忘了考虑的 case,测试也不会覆盖。比如上面那段实现,AI 没想到"全空格字符串 ' '” 该不该算空,所以测试里也没这个 case。强约束 TDD 下,AI 写测试时是从需求文本出发的,更可能列出"边界条件清单"再写测试,发现"哦,' ' 这个 case 需求里没说,要先问用户”。
设计反馈这行更微妙。我观察过几次实际跑:当强约束 TDD 跑到 Cycle 3、Cycle 4 时,AI 会主动说"这个测试写起来很痛,可能 API 设计有问题,我建议把 Tag(name=...) 改成 Tag.from_input(...) 返回 Result 类型"。这种反馈在自由 TDD 里几乎不会出现,因为它没经历过"写测试时发现 API 难用"的痛。约束让它被迫感受这种痛。
重构步骤最容易被忽略。自由 TDD 给你的代码通常是 GREEN 阶段产物:能跑、丑、变量名随意。强约束流程里 REFACTOR 是单独一阶段,AI 必须主动重命名、提炼方法、消除重复。我跑下来的代码可读性肉眼可见地高一档。
五、我的选择和实际用法
我现在做日常项目时默认开 strict 模式的 TDD skill:
tdd:
enforcement: strict
framework: pytest
run_tests:
- before_implementation
- after_implementation
before_implementation 这条是关键。它强制 AI 在写实现前先把测试跑一遍。这就是让 FAILED 成为可观察事件的机制。
慢吗?慢。一个原本 AI 一次输出能搞定的功能,强约束下要拆成 3-5 个 cycle,每个 cycle 都要等 pytest 跑完。我目测平均慢 2-3 倍,没精确测过。
但产出的代码我更敢直接合进 main 分支。自由 TDD 给我的代码我还会再读一遍测试,怀疑测试是不是按实现倒推写的。强约束 TDD 给我的代码至少过程上是诚实的,commit history、终端日志、cycle 之间的对话片段都能复盘。
跑久了之后还有一个副产品:我反而开始喜欢这种慢。它逼我把需求拆得更细。原本一句话扔给 Claude 让它自己理解的功能,强约束 TDD 下我必须先把"第一个 cycle 测什么"想清楚,等于先把需求拆了一遍。这种拆需求的训练效果不在 skill 的设计目标里,但价值不低。
六、什么时候反过来选
我并不在所有场景都开 TDD skill。有几类情况开了反而拖累。
探索式编程。我还不确定这套设计对不对,想先快速做一版能跑的看看效果。这时 TDD 的"测试约束实现"反而锁死了尝试不同方向的自由度。先跑通,再补测试,更合适。
性能调优。“这段代码应该多快"很难写成断言。强行 TDD 会得到一堆 assert duration < 100ms 的脆弱测试,跑在不同机器上结果不一样。性能问题该有专门的 benchmark,不是 unit test。
调外部不确定系统。集成第三方 API 时强制 TDD 会逼出大量 mock,最后测的是 mock 而不是真行为。文档说返回 A,但他们偶尔返回 B,你的 mock 永远不会发现。这种代码我宁可写一个端到端冒烟测试,不走 RED-GREEN-REFACTOR。
TDD skill 在"需求清楚、实现复杂、可重复运行"的代码里效果最好。不是哪都该开,是该挑场景开。
七、留白
我还没想清楚的:当模型继续变强,能不能像顶级工程师那样主动选择 RED-GREEN-REFACTOR 而不需要外部强制?理论上可以,但前提是它的训练目标里要加入"过程纪律"作为奖励信号。这跟当前主流的"输出质量奖励"是另一个维度的事,不是加 prompt 能解决。
短期内我会继续用 skill 强约束。如果你试过别的让 AI 自觉做 TDD 的方法,告诉我。
系列导航: