skills/superpowers/test-driven-development/SKILL.md
在实现任何 feature 或 bugfix 时使用;在写实现代码之前必须先写测试并看到它失败(TDD)。
npx skillsauth add lyfe2025/lyfes-coding-skills test-driven-developmentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
先写测试。看它失败。再写最小代码让它通过。
核心原则: 如果你没有亲眼看过测试失败,你就不知道它是否真的在测试正确的东西。
只遵守字面、不遵守精神,就是在违反规则。
永远:
例外(必须问 human partner):
如果你脑子里出现“就这一次跳过 TDD 吧”——停。那是在自我合理化。
没有先失败的测试,就禁止写 production code
先写了代码再写测试?删掉。重来。
没有例外:
从测试重新实现。就这样。
digraph tdd_cycle {
rankdir=LR;
red [label="RED\n写一个会失败的测试", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="确认失败\n且失败原因正确", shape=diamond];
green [label="GREEN\n最小实现代码", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="确认通过\n全绿", shape=diamond];
refactor [label="REFACTOR\n清理与重构", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="失败原因\n不对"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="保持\n全绿"];
verify_green -> next;
next -> red;
}
写一个最小测试,明确“应该发生什么”。
<Good> ```typescript test('失败时会重试 3 次', async () => { let attempts = 0; const operation = () => { attempts++; if (attempts < 3) throw new Error('fail'); return 'success'; };const result = await retryOperation(operation);
expect(result).toBe('success'); expect(attempts).toBe(3); });
命名清晰、测真实行为、只测一件事
</Good>
<Bad>
```typescript
test('重试能工作', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
命名含糊、测的是 mock 而不是代码行为 </Bad>
要求:
必须做。永远不要跳过。
npm test path/to/test.test.ts
确认:
测试直接通过? 你在测试既有行为。修测试。
测试报错? 修报错,反复运行直到它“正确地失败”。
写刚好能让测试通过的最简单代码。
<Good> ```typescript async function retryOperation<T>(fn: () => Promise<T>): Promise<T> { for (let i = 0; i < 3; i++) { try { return await fn(); } catch (e) { if (i === 2) throw e; } } throw new Error('unreachable'); } ``` 刚好足够通过测试 </Good> <Bad> ```typescript async function retryOperation<T>( fn: () => Promise<T>, options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (attempt: number) => void; } ): Promise<T> { // YAGNI } ``` 过度设计 </Bad>不要在这里加功能、重构别的代码、或“顺手优化”超出测试需求的东西。
必须做。
npm test path/to/test.test.ts
确认:
测试失败? 修代码,不要修测试。
其他测试失败? 现在就修。
只有在全绿之后才做:
保持 tests 全绿。不要引入新行为。
为下一个功能点写下一个 failing test。
| 维度 | 好 | 坏 |
|------|----|----|
| Minimal | 一次只测一件事;名字里出现 “and”?拆开 | test('校验邮箱和域名和空白') |
| Clear | 名字描述行为 | test('test1') |
| Shows intent | 展示期望 API/用法 | 把期望行为藏起来 |
“我先写实现,之后再补测试验证”
事后写的测试通常会立刻通过;“立刻通过”证明不了任何事:
test-first 强迫你看到测试先失败,从而证明它确实在测试某个东西。
“我已经手动测过所有边界条件了”
手测是临时的、随机的:
自动化测试是系统化的,每次都一样地运行。
“删掉 X 小时的工作太浪费了”
沉没成本谬误。时间已经花掉了。你现在的选择是:
真正的浪费是保留“你无法信任”的代码。没有真实测试的“能跑”只是技术债。
“TDD 太教条,务实就该灵活点”
TDD 才是务实:
所谓“务实捷径”= 线上 debug = 更慢。
“事后测试也能达到同样目的——重要的是精神不是仪式”
不行。事后测试回答“它现在做了什么”;test-first 回答“它应该做什么”。
事后测试会被你的实现带偏:你会测试你写出来的东西,而不是需求要求的东西。你只会验证你记得的边界条件,而不是在实现前被迫发现的边界条件。
30 分钟的事后补测 ≠ TDD:你可能得到覆盖率,但失去“测试确实能抓 bug”的证明。
| 借口 | 现实 | |------|------| | “太简单了不值得测” | 简单代码也会坏;写个测试 30 秒。 | | “我之后再测” | 立刻通过的测试证明不了任何事。 | | “事后测试也一样” | 事后:what does this do;事前:what should this do。 | | “我已经手测过了” | 临时 ≠ 系统;无记录、不可复跑。 | | “删掉 X 小时太浪费” | 沉没成本;保留未验证代码是技术债。 | | “留着当参考,先写测试” | 你会忍不住照着改;那就是事后测试。删就是删。 | | “我得先探索一下” | 可以。探索完就丢掉,从 TDD 开始。 | | “难测说明测试麻烦” | 难测 = 设计不清晰/难用;听测试的。 | | “TDD 会拖慢我” | TDD 比 debug 更快;务实 = test-first。 | | “手测更快” | 手测无法证明边界条件;每次改动都要重复测。 | | “旧代码本来就没测试” | 你是在改进它;从你改的地方开始补测试。 |
看到这些就意味着:删掉代码。用 TDD 重来。
Bug: 空邮箱被接受
RED
test('拒绝空邮箱', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('需要邮箱');
});
Verify RED
$ npm test
FAIL: expected '需要邮箱', got undefined
GREEN
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: '需要邮箱' };
}
// ...
}
Verify GREEN
$ npm test
PASS
REFACTOR 如需要,可抽出多字段共享的 validation。
在标记“完成”之前逐项检查:
任何一项打不了勾?你跳过了 TDD。重来。
| 问题 | 解决方式 | |------|----------| | 不知道怎么测 | 先写“你希望的 API”,先写断言;问 human partner。 | | 测试太复杂 | 设计太复杂。简化接口。 | | 必须 mock 一切 | 代码耦合太深。引入 dependency injection。 | | 测试 setup 巨大 | 抽 helper;仍复杂就简化设计。 |
发现 bug?先写能复现的 failing test,然后走 TDD cycle。测试既证明修复有效,也防止 regression。
永远不要在没有测试的情况下修 bug。
当你要引入 mocks 或 test utilities 时,先读 @testing-anti-patterns.md,避免常见陷阱:
Production code → 必须先存在且先失败过的测试
否则 → 不是 TDD
除非 human partner 明确许可,否则没有例外。
tools
在编写 skill 内容、验证 skill 是否有效、或需要用 TDD 方法测试 skill 能否被正确遵守时使用。
tools
当你有 spec/requirements 且任务需要多步推进时使用;在动代码之前先写出可执行的 implementation plan。
tools
在你准备声称“已完成/已修复/已通过”之前使用(尤其在 commit 或提 PR 前):必须运行 verification 命令并核对输出;永远 Evidence before assertions。
tools
在任何对话开始时使用:建立“如何发现并使用 skills”的规则,要求在任何回应(包括澄清问题)之前先 invoke Skill tool。