关于 drill-tdd
drill-tdd 是面向功能开发、修复缺陷和重构的测试驱动开发技能,要求先观察测试失败,再编写最少的生产代码,并在测试通过后重构。
主要功能
- 01先编写失败测试
- 02确认失败原因符合预期
- 03编写通过测试所需的最少代码
- 04重构前运行测试
- 05通过测试复现缺陷
适用场景
- 01按测试优先流程开发新功能
- 02修复缺陷并防止回归
- 03在测试通过的前提下重构
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ kucherenko/ gangsta --skill drill-tdd安装后在 Claude Code 中输入/drill-tdd确认已加载。
详细介绍
训练:测试驱动开发
概述
先编写测试。观察它失败。编写最少的代码使其通过。
核心原则: 如果你没有亲眼看到测试失败,就不知道它是否测试了正确的内容。
违反规则的字面要求,就是违背规则的精神。
何时使用
始终适用:
- 新功能
- Bug 修复
- 重构
- 行为变更
例外情况(请示 Don):
- 一次性原型
- 生成的代码
- 配置文件
在想“就这一次跳过训练”?停下。这是在自我合理化。
铁律(测试的沉默法则)
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
先写代码再写测试?删掉它。从头开始。
没有例外:
- 不要把它留作“参考”
- 编写测试时不要“改造”它
- 不要看它
- 删除就是删除
根据测试重新实现。就这样。
红—绿—重构
digraph tdd_cycle {
rankdir=LR;
red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="Verify fails\ncorrectly", shape=diamond];
green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="Verify passes\nAll green", shape=diamond];
refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="wrong\nfailure"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="stay\ngreen"];
verify_green -> next;
next -> red;
}
RED — 编写失败的测试
编写一个最小的测试,展示应该发生什么。
```typescript test('retries failed operations 3 times', 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('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
名称含糊,测试的是模拟对象而非代码
要求:
- 每个测试只测试一种行为
- 名称清晰且具有描述性
- 使用真实代码(除非无法避免,否则不要使用模拟对象)
验证 RED — 观察它失败
必须执行。绝不可跳过。
运行测试。确认:
- 测试失败(不是报错)
- 失败信息符合预期
- 失败是因为功能缺失(而不是拼写错误)
测试通过了? 你测试的是已有行为。修改测试。
测试报错了? 修复错误,重新运行,直到它正确地失败。
GREEN — 编写最少的代码
编写最简单的代码,使测试通过。
```typescript async function retryOperation(fn: () => Promise): Promise { for (let i = 0; i < 3; i++) { try { return await fn(); } catch (e) { if (i === 2) throw e; } } throw new Error('unreachable'); } ``` 只写足以通过测试的代码 ```typescript async function retryOperation( fn: () => Promise, options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; onRetry?: (attempt: number) => void; } ): Promise { // YAGNI — over-engineered } ``` 过度设计不要添加功能、重构其他代码,也不要超出测试要求去“改进”。
验证 GREEN — 观察它通过
必须执行。
运行测试。确认:
- 测试通过
- 其他测试仍然通过
- 输出干净(没有错误或警告)
测试失败了? 修复代码,而不是测试。
其他测试失败了? 现在就修复。
REFACTOR — 清理代码
只有通过测试后才能:
- 消除重复
- 改进名称
- 提取辅助函数
保持测试通过。不要添加行为。
重复
为下一个行为编写下一个失败的测试。
好的测试
| 质量 | 好 | 差 |
|---|---|---|
| 精简 | 只测试一件事。名称里有“和”?拆开。 | test('validates email and domain and whitespace') |
| 清晰 | 名称描述行为 | test('test1') |
| 表达意图 | 展示所需的 API | 模糊不清,不知道代码应该做什么 |
为什么顺序很重要
“我会之后再写测试,验证它能正常工作。”
代码写完后再写的测试会立即通过。立即通过证明不了任何事:
- 可能测试了错误的内容
- 可能测试的是实现,而不是行为
- 可能遗漏你没想到的边界情况
- 你从未看到它捕获 Bug
先写测试会迫使你看到测试失败,从而证明它确实测试了某些内容。
“删除 X 小时的工作太浪费了。”
这是沉没成本谬误。时间已经花掉了。现在你可以选择:
- 删除并用 TDD 重写(信心更高)
- 保留代码并在之后添加测试(信心较低,也更可能有 Bug)
真正的“浪费”,是保留你无法信任的代码。
“TDD 是教条主义的,务实就是灵活变通。”
TDD 本身就很务实:
- 在提交前发现 Bug(比事后调试更快)
- 防止回归(测试会立即发现问题)
- 记录行为(测试展示了如何使用代码)
- 支持重构(可以自由修改,测试会发现问题)
“务实”的捷径 = 在生产环境中调试 = 更慢。
常见借口
| 借口 | 现实 |
|---|---|
| “太简单,不值得测试” | 简单的代码也会出错。测试只需 30 秒。 |
| “我之后再测试” | 测试立即通过说明不了任何问题。 |
| “已经手动测试过了” | 临时测试 ≠ 系统化测试。没有记录,也无法重新运行。 |
| “删掉 X 小时的成果太浪费了” | 沉没成本谬误。保留未经验证的代码就是负债。 |
| “留作参考,先写测试” | 你会在它的基础上改。那就是先实现后测试。删掉就是彻底删除。 |
| “得先探索一下” | 没问题。丢弃探索成果,从 Drill 开始。 |
| “测试很难写 = 设计不清晰” | 听听测试的反馈。难以测试 = 难以使用。 |
| “TDD 会拖慢我” | Drill 比调试更快。 |
| “现有代码没有测试” | 你正在改进它。为你改动的部分添加测试。 |
危险信号 — 停下并重新开始
- 先写代码,再写测试
- 实现之后才写测试
- 测试立即通过
- 无法解释测试失败的原因
- 测试“稍后”再补
- 为“就这一次”找借口
- “留作参考”或“沿用现有代码”
- “已经花了 X 小时,删掉太浪费了”
- “TDD 太教条了,我这是讲求实际”
以上任何一种情况都意味着:删除代码。从 Drill 重新开始。
示例:修复 Bug
Bug: 接受空电子邮件地址
RED
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
验证 RED
$ npm test
FAIL: expected 'Email required', got undefined
GREEN
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...existing code
}
验证 GREEN
$ npm test
PASS
REFACTOR — 如有需要,为多个字段提取验证逻辑。
验证清单
标记工作完成前:
- 每个新增函数/方法都有测试
- 实现前观察过每个测试失败
- 每个测试都因预期原因失败(功能缺失,而非拼写错误)
- 编写了通过每个测试所需的最少代码
- 所有测试均通过
- 输出干净无误(没有错误或警告)
- 测试使用真实代码(只有不得已时才使用模拟对象)
- 已覆盖边界情况和错误
无法勾选所有项?你跳过了 Drill。重新开始。
集成调试
发现 Bug?编写失败测试来复现它。遵循 Drill 循环。测试既能证明修复有效,也能防止回归。
绝不要在没有测试的情况下修复 Bug。先使用 gangsta:interrogation-debugging 调查根本原因。
遇到困难时
| 问题 | 解决方案 |
|---|---|
| 不知道怎么测试 | 写出你希望拥有的 API。先写断言。向 Don 请教。 |
| 测试太复杂 | 设计太复杂。简化接口。 |
| 什么都必须模拟 | 代码耦合过紧。使用依赖注入。 |
| 测试设置太庞大 | 提取辅助方法。仍然复杂?简化设计。 |
Omerta 合规
- 真相原则:测试输出是实际命令输出,而非口头声称
- 规范即法律:每项实现都能追溯到测试,而测试能追溯到需求