test-driven-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese测试驱动开发(TDD)
Test-Driven Development (TDD)
概述
Overview
先写测试。看它失败。写最少的代码让它通过。
核心原则: 如果你没有看到测试失败,你就不知道它是否测试了正确的东西。
违反规则的字面意思就是违反规则的精神。
Write tests first. Watch it fail. Write the minimal code to make it pass.
Core Principle: If you don't see the test fail, you don't know if it's testing the right thing.
Violating the letter of the rule is violating the spirit of the rule.
何时使用
When to Use
始终使用:
- 新功能
- Bug 修复
- 重构
- 行为变更
例外(需询问你的人类伙伴):
- 一次性原型
- 生成的代码
- 配置文件
想着"就这一次跳过 TDD"?停下来。那是在给自己找借口。
Always Use:
- New features
- Bug fixes
- Refactoring
- Behavior changes
Exceptions (Ask your human partner):
- One-off prototypes
- Generated code
- Configuration files
Thinking "I'll skip TDD just this once"? Stop. That's making excuses.
铁律
Iron Law
没有失败的测试,就不写生产代码先写了代码再写测试?删掉它。从头来过。
没有例外:
- 不要保留作为"参考"
- 不要在写测试时"改编"它
- 不要看它
- 删除就是删除
从测试出发,重新实现。句号。
No production code is written without a failing testWrote code before tests? Delete it. Start over.
No exceptions:
- Don't keep it as "reference"
- Don't "adapt" it while writing tests
- Don't look at it
- Delete means delete
Reimplement starting from tests. Period.
红-绿-重构
Red-Green-Refactor
dot
digraph tdd_cycle {
rankdir=LR;
red [label="红灯\n编写失败的测试", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="验证正确失败", shape=diamond];
green [label="绿灯\n最少代码", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="验证通过\n全部绿灯", shape=diamond];
refactor [label="重构\n清理代码", shape=box, style=filled, fillcolor="#ccccff"];
next [label="下一个", shape=ellipse];
red -> verify_red;
verify_red -> green [label="是"];
verify_red -> red [label="错误的\n失败"];
green -> verify_green;
verify_green -> refactor [label="是"];
verify_green -> green [label="否"];
refactor -> verify_green [label="保持\n绿灯"];
verify_green -> next;
next -> red;
}dot
digraph tdd_cycle {
rankdir=LR;
red [label="Red\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="Verify correct failure", shape=diamond];
green [label="Green\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="Verify pass\nAll green", shape=diamond];
refactor [label="Refactor\nClean up code", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next", shape=ellipse];
red -> verify_red;
verify_red -> green [label="Yes"];
verify_red -> red [label="Incorrect\nfailure"];
green -> verify_green;
verify_green -> refactor [label="Yes"];
verify_green -> green [label="No"];
refactor -> verify_green [label="Keep\ngreen"];
verify_green -> next;
next -> red;
}红灯 - 编写失败的测试
Red - Write Failing Test
写一个最小的测试来展示期望行为。
<Good>
```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);
});名称模糊,测试的是 mock 而非代码
</Bad>
要求:
- 一个行为
- 清晰的名称
- 使用真实代码(除非不得已才用 mock)
Write a minimal test that demonstrates the desired behavior.
<Good>
```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);
});
Clear name, tests real behavior, tests only one thing
</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);
});Vague name, tests mock instead of code
</Bad>
Requirements:
- One behavior
- Clear name
- Use real code (only use mock if absolutely necessary)
验证红灯 - 看它失败
Verify Red - Watch It Fail
必须执行。绝不跳过。
bash
npm test path/to/test.test.ts确认:
- 测试失败(不是报错)
- 失败信息符合预期
- 失败原因是功能缺失(不是拼写错误)
测试通过了? 你在测试已有的行为。修改测试。
测试报错了? 修复错误,重新运行直到它正确地失败。
Must execute. Never skip.
bash
npm test path/to/test.test.tsConfirm:
- Test fails (not errors)
- Failure message matches expectations
- Failure reason is missing functionality (not typo)
Test passed? You're testing existing behavior. Modify the test.
Test errored? Fix the error, rerun until it fails correctly.
绿灯 - 最少代码
Green - Minimal Code
写最简单的代码让测试通过。
<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>
不要添加功能、重构其他代码或做超出测试要求的"改进"。
Write the simplest code to make the test pass.
<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');
}
```
Just enough to pass the test
</Good>
<Bad>
```typescript
async function retryOperation<T>(
fn: () => Promise<T>,
options?: {
maxRetries?: number;
backoff?: 'linear' | 'exponential';
onRetry?: (attempt: number) => void;
}
): Promise<T> {
// YAGNI
}
```
Over-engineered
</Bad>
Don't add features, refactor other code, or make "improvements" beyond what the test requires.
验证绿灯 - 看它通过
Verify Green - Watch It Pass
必须执行。
bash
npm test path/to/test.test.ts确认:
- 测试通过
- 其他测试仍然通过
- 输出干净(没有错误、警告)
测试失败了? 修改代码,不是测试。
其他测试失败了? 立即修复。
Must execute.
bash
npm test path/to/test.test.tsConfirm:
- Test passes
- Other tests still pass
- Output is clean (no errors, warnings)
Test failed? Modify the code, not the test.
Other tests failed? Fix immediately.
重构 - 清理代码
Refactor - Clean Up Code
只有在绿灯之后才重构:
- 消除重复
- 改善命名
- 提取辅助函数
保持测试绿灯。不要添加行为。
Only refactor after green:
- Eliminate duplication
- Improve naming
- Extract helper functions
Keep tests green. Don't add behavior.
重复
Repeat
为下一个功能写下一个失败的测试。
Write the next failing test for the next feature.
好的测试
Good Tests
| 特质 | 好的 | 差的 |
|---|---|---|
| 最小化 | 只测一件事。名称中有"和"?拆分它。 | |
| 清晰 | 名称描述行为 | |
| 展示意图 | 展示期望的 API | 掩盖了代码应该做什么 |
| Characteristic | Good | Bad |
|---|---|---|
| Minimal | Tests only one thing. Name has "and"? Split it. | |
| Clear | Name describes behavior | |
| Demonstrates Intent | Shows expected API | Hides what the code should do |
为什么顺序很重要
Why Order Matters
"我先写完再补测试来验证"
后写的测试立即通过。立即通过什么也证明不了:
- 可能测试了错误的东西
- 可能测试的是实现而非行为
- 可能遗漏了你忘掉的边界情况
- 你从未看到它捕获 bug
先写测试迫使你看到测试失败,证明它确实在测试某些东西。
"我已经手动测试了所有边界情况"
手动测试是临时的。你以为你测试了所有情况,但是:
- 没有测试记录
- 代码变更后无法重新运行
- 在压力下容易遗忘
- "我试过了能跑" 不等于 全面测试
自动化测试是系统性的。它们每次以相同方式运行。
"删除 X 小时的工作太浪费了"
沉没成本谬误。时间已经花了。你现在的选择:
- 删除并用 TDD 重写(再花 X 小时,高信心)
- 保留并后补测试(30 分钟,低信心,可能有 bug)
"浪费"的是保留你无法信任的代码。没有真正测试的可运行代码就是技术债。
"TDD 太教条了,务实意味着灵活变通"
TDD 就是务实的:
- 在 commit 前发现 bug(比事后调试快)
- 防止回归(测试立即发现破坏)
- 记录行为(测试展示如何使用代码)
- 支持重构(放心修改,测试捕获破坏)
"务实的"捷径 = 在生产环境调试 = 更慢。
"后补测试也能达到相同目的——重要的是精神不是仪式"
不对。后补测试回答"这段代码做了什么?"先写测试回答"这段代码应该做什么?"
后补测试受你实现的偏见影响。你测试的是你构建的东西,而非需求要求的。你验证的是你记得的边界情况,而非发现的。
先写测试迫使你在实现前发现边界情况。后补测试验证的是你记住了所有情况(你没有)。
30 分钟的后补测试 ≠ TDD。你得到了覆盖率,但失去了测试有效的证明。
"I'll write tests later to verify after finishing"
Tests written afterwards pass immediately. Passing immediately proves nothing:
- Might be testing the wrong thing
- Might be testing implementation instead of behavior
- Might miss edge cases you forgot
- You never see it catch bugs
Writing tests first forces you to see the test fail, proving it's actually testing something.
"I've manually tested all edge cases"
Manual testing is temporary. You think you tested everything, but:
- No test record
- Can't rerun after code changes
- Easy to forget under pressure
- "I tried it and it works" ≠ comprehensive testing
Automated tests are systematic. They run the same way every time.
"Deleting X hours of work is too wasteful"
Sunk cost fallacy. The time is already spent. Your options now:
- Delete and rewrite with TDD (another X hours, high confidence)
- Keep and add tests later (30 minutes, low confidence, potential bugs)
The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
"TDD is too dogmatic, pragmatism means flexibility"
TDD is pragmatic:
- Find bugs before commit (faster than debugging later)
- Prevent regressions (tests catch breaks immediately)
- Document behavior (tests show how to use code)
- Support refactoring (modify with confidence, tests catch breaks)
"Pragmatic" shortcuts = debugging in production = slower.
"Writing tests later achieves the same purpose—what matters is the spirit not the ritual"
No. Tests written later answer "What does this code do?" Tests written first answer "What should this code do?"
Tests written later are biased by your implementation. You test what you built, not what the requirements demand. You verify edge cases you remembered, not those you discovered.
Writing tests first forces you to discover edge cases before implementation. Tests written later verify you remembered all cases (you didn't).
30 minutes of tests written later ≠ TDD. You get coverage, but lose proof that the tests are effective.
常见借口
Common Excuses
| 借口 | 现实 |
|---|---|
| "太简单了不用测" | 简单的代码也会出 bug。测试只需 30 秒。 |
| "我之后补测试" | 立即通过的测试什么也证明不了。 |
| "后补测试也能达到相同目的" | 后补测试 = "这做了什么?" 先写测试 = "这应该做什么?" |
| "已经手动测试过了" | 临时测试 ≠ 系统测试。无记录,无法重现。 |
| "删除 X 小时的工作太浪费" | 沉没成本谬误。保留未验证的代码就是技术债。 |
| "留作参考,然后先写测试" | 你会去改编它。那就是后补测试。删除就是删除。 |
| "需要先探索一下" | 可以。探索完了扔掉,从 TDD 开始。 |
| "测试难写 = 设计不清楚" | 听测试的。难以测试 = 难以使用。 |
| "TDD 会拖慢我" | TDD 比调试快。务实 = 先写测试。 |
| "手动测试更快" | 手动测试无法证明边界情况。每次修改你都得重新测。 |
| "现有代码没有测试" | 你在改进它。为现有代码补测试。 |
| Excuse | Reality |
|---|---|
| "It's too simple to test" | Simple code can still have bugs. Tests take 30 seconds. |
| "I'll add tests later" | Tests that pass immediately prove nothing. |
| "Writing tests later achieves the same purpose" | Tests later = "What does this do?" Tests first = "What should this do?" |
| "I've tested it manually" | Temporary testing ≠ systematic testing. No record, cannot reproduce. |
| "Deleting X hours of work is too wasteful" | Sunk cost fallacy. Keeping unvalidated code is technical debt. |
| "Keep it as reference, then write tests first" | You'll adapt it. That's writing tests later. Delete means delete. |
| "Need to explore first" | You can. After exploring, throw it away and start with TDD. |
| "Hard to test = unclear design" | Listen to the tests. Hard to test = hard to use. |
| "TDD slows me down" | TDD is faster than debugging. Pragmatism = write tests first. |
| "Manual testing is faster" | Manual testing can't prove edge cases. You have to retest every time you modify code. |
| "Existing code has no tests" | You're improving it. Add tests to existing code. |
危险信号 - 停下来,从头开始
Red Flags - Stop, Start Over
- 先写了代码再写测试
- 实现完了才补测试
- 测试立即通过
- 无法解释测试为什么失败
- "之后再补"测试
- 说服自己"就这一次"
- "我已经手动测试过了"
- "后补测试也能达到相同目的"
- "重要的是精神不是仪式"
- "留作参考"或"改编现有代码"
- "已经花了 X 小时了,删掉太浪费"
- "TDD 太教条了,我是在务实"
- "这次情况不同,因为……"
以上所有情况都意味着:删除代码。用 TDD 从头开始。
- Wrote code before tests
- Added tests after implementation
- Test passes immediately
- Can't explain why the test failed
- "I'll add tests later"
- Convincing yourself "just this once"
- "I've tested it manually"
- "Writing tests later achieves the same purpose"
- "What matters is the spirit not the ritual"
- "Keep as reference" or "adapt existing code"
- "I've spent X hours, deleting is too wasteful"
- "TDD is too dogmatic, I'm being pragmatic"
- "This situation is different because..."
All of the above mean: Delete the code. Start over with TDD.
示例:Bug 修复
Example: Bug Fix
Bug: 空邮箱被接受了
红灯
typescript
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});验证红灯
bash
$ npm test
FAIL: expected 'Email required', got undefined绿灯
typescript
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}验证绿灯
bash
$ npm test
PASS重构
如果需要,提取验证逻辑以支持多个字段。
Bug: Empty email is accepted
Red
typescript
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});Verify Red
bash
$ npm test
FAIL: expected 'Email required', got undefinedGreen
typescript
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}Verify Green
bash
$ npm test
PASSRefactor
If needed, extract validation logic to support multiple fields.
验证清单
Verification Checklist
在标记工作完成之前:
- 每个新函数/方法都有测试
- 在实现之前看到每个测试失败
- 每个测试因预期原因失败(功能缺失,不是拼写错误)
- 为每个测试编写了最少代码使其通过
- 所有测试通过
- 输出干净(没有错误、警告)
- 测试使用真实代码(只在不可避免时用 mock)
- 覆盖了边界情况和错误场景
不能全部勾选?你跳过了 TDD。从头开始。
Before marking work complete:
- Every new function/method has tests
- Saw each test fail before implementation
- Each test failed for the expected reason (missing functionality, not typo)
- Wrote minimal code for each test to pass
- All tests pass
- Output is clean (no errors, warnings)
- Tests use real code (only use mock when unavoidable)
- Covered edge cases and error scenarios
Can't check all boxes? You skipped TDD. Start over.
遇到困难时
When Stuck
| 问题 | 解决方案 |
|---|---|
| 不知道怎么测试 | 写出你期望的 API。先写断言。问你的人类伙伴。 |
| 测试太复杂 | 设计太复杂。简化接口。 |
| 必须 mock 所有东西 | 代码耦合太紧。使用依赖注入。 |
| 测试 setup 太庞大 | 提取辅助函数。还是复杂?简化设计。 |
| Problem | Solution |
|---|---|
| Don't know how to test | Write the API you expect. Write assertions first. Ask your human partner. |
| Tests are too complex | Design is too complex. Simplify the interface. |
| Have to mock everything | Code is too tightly coupled. Use dependency injection. |
| Test setup is too bulky | Extract helper functions. Still complex? Simplify design. |
调试集成
Debugging Integration
发现 bug?写一个重现 bug 的失败测试。按 TDD 循环走。测试既证明了修复有效,又防止了回归。
绝不在没有测试的情况下修复 bug。
Found a bug? Write a failing test that reproduces the bug. Follow the TDD cycle. The test both proves the fix works and prevents regressions.
Never fix a bug without a test.
测试反模式
Testing Anti-Patterns
添加 mock 或测试工具时,阅读 @testing-anti-patterns.md 以避免常见陷阱:
- 测试 mock 行为而非真实行为
- 在生产类中添加仅测试用的方法
- 在不理解依赖的情况下使用 mock
When adding mocks or testing tools, read @testing-anti-patterns.md to avoid common pitfalls:
- Testing mock behavior instead of real behavior
- Adding test-only methods to production classes
- Using mocks without understanding dependencies
最终规则
Final Rule
生产代码 → 测试存在且先失败
否则 → 不是 TDD没有你的人类伙伴的许可,没有例外。
Production code → Test exists and failed first
Otherwise → Not TDDNo exceptions without permission from your human partner.