关于 test-driven-development
面向实现或修复软件行为的开发者的测试驱动开发 Skill。它要求先观察测试失败,再编写会改变行为的生产代码,并遵循红—绿—重构循环。
主要功能
- 01行为变更前必须先有失败测试
- 02规定红—绿—重构循环
- 03说明有效测试失败的判定标准
- 04涵盖遗留代码特征测试
- 05提供确定性测试规则
适用场景
- 01先编写测试再实现功能
- 02先复现缺陷再进行修复
- 03用特征测试安全重构遗留代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ izyanrajwani/ agent-skills-library --skill test-driven-development安装后在 Claude Code 中输入/test-driven-development确认已加载。
附带文件references/tdd-philosophy.mdreferences/testing-anti-patterns.md
详细介绍
测试驱动开发
先写测试。观察测试失败。编写能通过测试的最少代码。然后重构。
核心原则: 如果你没有亲眼看到测试失败,就不知道它是否测对了。
铁律
NO BEHAVIOR-CHANGING PRODUCTION CODE WITHOUT A FAILING TEST FIRST
先写了代码再写测试?彻底删除代码。从测试重新开始实现。
重构除外: 重构步骤改变的是结构,而不是行为。测试始终保持通过。无需新增失败测试。
红—绿—重构循环
RED ──► Verify Fail ──► GREEN ──► Verify Pass ──► REFACTOR ──► Verify Pass ──► Next RED
│ │ │
▼ ▼ ▼
Wrong failure? Still failing? Broke tests?
Fix test, retry Fix code, retry Fix, retry
红 — 编写失败的测试
针对一种行为编写一个最简测试。
好示例:
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = async () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
名称清晰,测试真实行为,断言可观察到的结果
坏示例:
test('retry works', async () => {
const mock = jest.fn()
.mockRejectedValueOnce(new Error())
.mockRejectedValueOnce(new Error())
.mockResolvedValueOnce('success');
await retryOperation(mock);
expect(mock).toHaveBeenCalledTimes(3);
});
名称含糊,只断言调用次数而不验证结果,测试的是模拟机制而非行为
要求: 一种行为。名称清晰。使用真实代码(只有在无法避免时才使用模拟)。
验证红 — 观察测试失败
必须执行。绝不跳过。
npm test path/to/test.test.ts
测试必须因正确的原因变红。可接受的红色状态:
- 断言失败(预期行为尚未实现)
- 编译/类型错误(函数尚不存在)
不可接受:运行时初始化错误、导入失败、环境问题。
测试立即通过?你测的是已有行为——修正测试。 测试因错误的原因报错?修复错误,重新运行,直到它正确失败。
绿 — 编写最少代码
编写能通过测试的最简单代码。
好示例:
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');
}
刚好足以通过测试
坏示例:
async function retryOperation<T>(
fn: () => Promise<T>,
options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; }
): Promise<T> { /* YAGNI */ }
实现过度,超出测试要求
只编写测试所要求的内容。不添加额外功能,不做“改进”。
验证绿 — 观察测试通过
必须执行。
npm test path/to/test.test.ts
确认:测试通过。其他所有测试仍然通过。输出干净(没有错误或警告)。
测试失败?修复代码,不要改测试。 其他测试失败?继续之前立即修复。
重构 — 清理代码
只有测试通过后才能重构:消除重复。改进命名。提取辅助函数。
始终保持测试通过。不添加新行为。
重复
针对下一种行为编写下一个失败的测试。
好测试
最小化: 每个测试只测一件事。名称里有“和”?拆开。❌ test('validates email and domain and whitespace')
清晰: 名称描述行为。❌ test('test1')
体现意图: 展示期望的 API 用法,而不是实现细节。
示例:修复缺陷
缺陷: 接受空邮箱
红:
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});
验证红:
$ npm test
FAIL: expected 'Email required', got undefined
绿:
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}
验证绿:
$ npm test
PASS
重构: 如果这种模式重复出现,就提取验证辅助函数。
危险信号 — 停止并重新开始
出现以下任一情况,都意味着应删除代码并从 TDD 重新开始:
- 先写了代码,再写测试
- 测试立即通过(测试的是已有行为)
- 无法解释测试为何失败
- 为“就这一次”或“情况不同”找理由
- 写测试时把代码留着“当参考”
- 声称“事后补测试也能达到同样目的”
遇到困难时
| 问题 | 解决方案 |
|---|---|
| 不知道如何测试 | 写出你希望存在的 API。先写断言。 |
| 测试太复杂 | 设计太复杂。简化接口。 |
| 什么都必须模拟 | 代码耦合过紧。引入依赖注入。 |
| 测试初始化内容太多 | 提取辅助函数。还是复杂?简化设计。 |
遗留代码(没有现有测试)
铁律(“删除并重新开始”)适用于你在没有测试的情况下编写的新代码。对于没有测试的继承代码,请使用特征测试:
- 编写测试以记录当前行为(即使它“有误”)
- 运行测试,观察实际输出
- 更新断言以匹配实际情况(这些就是“黄金主测试”)
- 现在你有了一道安全网,可以放心重构
- 对新的行为变更应用 TDD
特征测试会锁定现有行为,让你能够安全地重构。它们是入门途径,而非永久状态。
不稳定性规则
测试必须具有确定性。单元测试中禁止以下做法:
- 真实睡眠/延迟 → 使用模拟计时器(
vi.useFakeTimers()、jest.useFakeTimers()) - 墙上时钟时间 → 注入时钟,并针对注入的时间进行断言
- Math.random() → 为 RNG 设定种子或注入 RNG
- 网络调用 → 在边界处模拟,或使用 MSW
- 文件系统竞态条件 → 使用名称唯一的临时目录
测试不稳定?修复它或删除它。不稳定的测试会侵蚀整个测试套件的可信度。
调试集成
发现 Bug?先编写一个能复现它的失败测试。然后遵循 TDD 循环。测试既能证明修复有效,也能防止回归。
规划:测试列表
开始循环之前,花 2 分钟列出接下来预计要编写的 3-10 个测试。这可以避免局部最优设计,防止早期测试让你陷入困境。
重试函数的测试列表示例:
- 失败时重试 N 次
- 成功时返回结果
- 达到最大重试次数后抛出异常
- 在多次尝试之间调用 onRetry 回调
- 遵循退避延迟
按顺序完成列表中的测试。随着了解的深入,添加或移除测试。
测试反模式
编写涉及模拟对象、依赖项或测试工具的测试时:请参阅 references/testing-anti-patterns.md,了解常见陷阱,包括测试模拟行为,以及向生产类添加仅供测试使用的方法。
理念与合理化
有关对常见异议(“我之后再测试”、“删除成果很浪费”、“TDD 教条”)的详细反驳:请参阅 references/tdd-philosophy.md
最终规则
Production code exists → test existed first and failed first
Otherwise → not TDD