test-driven-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTest-Driven Development (TDD)
测试驱动开发(TDD)
Overview
概述
Write the test first. Watch it fail. Write minimal code to pass.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
Violating the letter of the rules is violating the spirit of the rules.
先编写测试,观察测试失败,再编写最少代码让测试通过。
核心原则: 如果没有看到测试失败,你无法确定测试是否针对正确的内容。
违反规则的字面要求就是违反规则的精神。
When to Use
适用场景
Always:
- New features
- Bug fixes
- Refactoring
- Behavior changes
Exceptions (ask your human partner):
- Throwaway prototypes
- Generated code
- Configuration files
Thinking "skip TDD just this once"? Stop. That's rationalization.
始终适用:
- 新功能开发
- Bug修复
- 代码重构
- 行为变更
例外情况(需征得人工伙伴同意):
- 一次性原型
- 自动生成的代码
- 配置文件
想着“就这一次跳过TDD”?别这么做。这只是自我合理化的借口。
The Iron Law
铁律
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRSTWrite code before the test? 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
Implement fresh from tests. Period.
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST先写了代码再写测试?删掉代码,重新开始。
无例外:
- 不要把它当作“参考”保留
- 不要在写测试时“改编”它
- 不要看它
- 删除就是彻底删除
从测试开始重新实现。就这么简单。
Red-Green-Refactor
红-绿-重构
dot
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;
}dot
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 - Write Failing Test
RED - 编写失败的测试
Write one minimal test showing what should happen.
<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, 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 not code
</Bad>
Requirements:
- One behavior
- Clear name
- Real code (no mocks unless unavoidable)
编写一个最小化的测试,明确预期行为。
<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)
Verify RED - Watch It Fail
验证RED状态 - 观察测试失败
MANDATORY. Never skip.
bash
npm test path/to/test.test.tsConfirm:
- Test fails (not errors)
- Failure message is expected
- Fails because feature missing (not typos)
Test passes? You're testing existing behavior. Fix test.
Test errors? Fix error, re-run until it fails correctly.
必须执行,绝不跳过。
bash
npm test path/to/test.test.ts确认:
- 测试失败(而非报错)
- 失败信息符合预期
- 失败原因是功能缺失(而非拼写错误)
测试通过了? 你在测试已有的行为,修改测试。
测试报错了? 修复错误,重新运行直到测试正确失败。
GREEN - Minimal Code
GREEN - 编写最小化代码
Write simplest code to pass the test.
<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
</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 "improve" beyond the test.
编写最简单的代码让测试通过。
<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(You Aren't Gonna Need It,你不会用到它)
}
```
过度设计
</Bad>
不要添加额外功能、重构其他代码或做超出测试要求的“优化”。
Verify GREEN - Watch It Pass
验证GREEN状态 - 观察测试通过
MANDATORY.
bash
npm test path/to/test.test.tsConfirm:
- Test passes
- Other tests still pass
- Output pristine (no errors, warnings)
Test fails? Fix code, not test.
Other tests fail? Fix now.
必须执行。
bash
npm test path/to/test.test.ts确认:
- 当前测试通过
- 其他测试仍能通过
- 输出干净(无错误、警告)
测试失败? 修复代码,而非测试。
其他测试失败? 立即修复。
REFACTOR - Clean Up
REFACTOR - 代码清理
After green only:
- Remove duplication
- Improve names
- Extract helpers
Keep tests green. Don't add behavior.
仅在测试通过后进行:
- 消除重复代码
- 优化命名
- 提取辅助函数
保持测试通过,不添加新行为。
Repeat
重复循环
Next failing test for next feature.
为下一个功能点编写新的失败测试。
Good Tests
优质测试的标准
| Quality | Good | Bad |
|---|---|---|
| Minimal | One thing. "and" in name? Split it. | |
| Clear | Name describes behavior | |
| Shows intent | Demonstrates desired API | Obscures what code should do |
| 维度 | 优秀示例 | 糟糕示例 |
|---|---|---|
| 最小化 | 单一功能。名称中出现“和”?拆分测试。 | |
| 清晰性 | 名称描述行为 | |
| 意图明确 | 展示期望的API | 掩盖代码应有的行为 |
Why Order Matters
顺序的重要性
"I'll write tests after to verify it works"
Tests written after code pass immediately. Passing immediately proves nothing:
- Might test wrong thing
- Might test implementation, not behavior
- Might miss edge cases you forgot
- You never saw it catch the bug
Test-first forces you to see the test fail, proving it actually tests something.
"I already manually tested all the edge cases"
Manual testing is ad-hoc. You think you tested everything but:
- No record of what you tested
- Can't re-run when code changes
- Easy to forget cases under pressure
- "It worked when I tried it" ≠ comprehensive
Automated tests are systematic. They run the same way every time.
"Deleting X hours of work is wasteful"
Sunk cost fallacy. The time is already gone. Your choice now:
- Delete and rewrite with TDD (X more hours, high confidence)
- Keep it and add tests after (30 min, low confidence, likely bugs)
The "waste" is keeping code you can't trust. Working code without real tests is technical debt.
"TDD is dogmatic, being pragmatic means adapting"
TDD IS pragmatic:
- Finds bugs before commit (faster than debugging after)
- Prevents regressions (tests catch breaks immediately)
- Documents behavior (tests show how to use code)
- Enables refactoring (change freely, tests catch breaks)
"Pragmatic" shortcuts = debugging in production = slower.
"Tests after achieve the same goals - it's spirit not ritual"
No. Tests-after answer "What does this do?" Tests-first answer "What should this do?"
Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones.
Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't).
30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work.
“我会先写代码,之后再写测试验证”
代码完成后写的测试会立即通过,而立即通过的测试无法证明任何事情:
- 可能测试了错误的内容
- 可能测试的是实现细节而非行为
- 可能遗漏了你忘记的边缘情况
- 你从未见过它捕获Bug
先写测试会迫使你看到测试失败,证明测试确实能检测到问题。
“我已经手动测试了所有边缘情况”
手动测试是随机的。你以为测试了所有情况,但:
- 没有记录测试内容
- 代码变更后无法重复执行
- 压力下容易遗忘测试用例
- “我试的时候是好的”≠全面覆盖
自动化测试是系统化的,每次运行都保持一致。
“删除X小时的工作是浪费”
这是沉没成本谬误。时间已经花了,现在你可以选择:
- 删除代码,用TDD重写(多花X小时,获得高可信度)
- 保留代码,之后补测试(花30分钟,可信度低,可能存在Bug)
“浪费”的是保留你无法信任的代码。没有可靠测试的工作代码是技术债务。
“TDD太教条,务实意味着灵活调整”
TDD本身就是务实的:
- 提交前发现Bug(比事后调试更快)
- 防止回归(代码变更时测试立即捕获问题)
- 文档化行为(测试展示代码的使用方式)
- 支持重构(自由修改代码,测试捕获问题)
“务实”的捷径=生产环境调试=更慢的开发。
“事后写测试也能达到同样目的——重要的是精神而非形式”
并非如此。事后写的测试回答“这段代码做什么?”,而先写的测试回答“这段代码应该做什么?”
事后写测试会受现有实现的影响,你测试的是已构建的内容,而非需求。你验证的是你记得的边缘情况,而非发现的新情况。
先写测试会迫使你在实现前发现边缘情况。事后写测试只能验证你记得的内容(而你肯定会遗漏)。
30分钟的事后测试≠TDD。你获得了覆盖率,但失去了测试有效的证明。
Common Rationalizations
常见自我合理化借口
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. Test takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. |
| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. |
| "Existing code has no tests" | You're improving it. Add tests for existing code. |
| 借口 | 真相 |
|---|---|
| “太简单了不需要测试” | 简单代码也会出错。写测试只需要30秒。 |
| “我之后再写测试” | 立即通过的测试无法证明任何事情。 |
| “事后写测试效果一样” | 事后测试=“代码做什么?”,先写测试=“代码应该做什么?” |
| “已经手动测试过了” | 随机测试≠系统化测试。无记录,无法重复执行。 |
| “删除X小时的工作太浪费” | 沉没成本谬误。保留未验证的代码是技术债务。 |
| “保留代码当参考,先写测试” | 你会不自觉地参考它,这本质还是事后测试。删除就是彻底删除。 |
| “需要先探索一下” | 没问题。探索完成后扔掉代码,用TDD重新开始。 |
| “测试难度大=设计不清晰” | 倾听测试的反馈。难测试的代码通常难使用。 |
| “TDD会拖慢进度” | TDD比调试更快。务实的做法就是先写测试。 |
| “手动测试更快” | 手动测试无法覆盖所有边缘情况。代码变更后你需要重复手动测试。 |
| “现有代码没有测试” | 你正在改进它,为现有代码添加测试。 |
Red Flags - STOP and Start Over
危险信号 - 停止并重新开始
- Code before test
- Test after implementation
- Test passes immediately
- Can't explain why test failed
- Tests added "later"
- Rationalizing "just this once"
- "I already manually tested it"
- "Tests after achieve the same purpose"
- "It's about spirit not ritual"
- "Keep as reference" or "adapt existing code"
- "Already spent X hours, deleting is wasteful"
- "TDD is dogmatic, I'm being pragmatic"
- "This is different because..."
All of these mean: Delete code. Start over with TDD.
- 先写了生产代码再写测试
- 实现完成后补测试
- 测试立即通过
- 无法解释测试失败的原因
- 测试是“后来添加的”
- 自我合理化“就这一次”
- “我已经手动测试过了”
- “事后写测试效果一样”
- “重要的是精神而非形式”
- “保留代码当参考”或“改编现有代码”
- “已经花了X小时,删除太浪费”
- “TDD太教条,我是务实的”
- “这次情况特殊因为……”
出现以上任何一种情况:删除代码,用TDD重新开始。
Example: Bug Fix
示例:Bug修复
Bug: Empty email 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
Extract validation for multiple fields if needed.
Bug: 空邮箱被允许提交
RED状态
typescript
test('rejects empty email', async () => {
const result = await submitForm({ email: '' });
expect(result.error).toBe('Email required');
});验证RED状态
bash
$ npm test
FAIL: expected 'Email required', got undefinedGREEN状态
typescript
function submitForm(data: FormData) {
if (!data.email?.trim()) {
return { error: 'Email required' };
}
// ...
}验证GREEN状态
bash
$ npm test
PASSREFACTOR状态
如果需要,提取验证逻辑用于多个字段。
Verification Checklist
验证清单
Before marking work complete:
- Every new function/method has a test
- Watched each test fail before implementing
- Each test failed for expected reason (feature missing, not typo)
- Wrote minimal code to pass each test
- All tests pass
- Output pristine (no errors, warnings)
- Tests use real code (mocks only if unavoidable)
- Edge cases and errors covered
Can't check all boxes? You skipped TDD. Start over.
标记工作完成前:
- 每个新函数/方法都有对应的测试
- 每个测试在实现前都观察到失败
- 每个测试的失败原因符合预期(功能缺失而非拼写错误)
- 编写的代码刚好满足测试要求
- 所有测试通过
- 输出干净(无错误、警告)
- 测试使用真实代码(仅在必要时使用Mock)
- 覆盖边缘情况和错误场景
无法勾选所有选项?说明你跳过了TDD,重新开始。
When Stuck
遇到困境时的解决方法
| Problem | Solution |
|---|---|
| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. |
| Test too complicated | Design too complicated. Simplify interface. |
| Must mock everything | Code too coupled. Use dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify design. |
| 问题 | 解决方案 |
|---|---|
| 不知道如何测试 | 编写期望的API,先写断言。咨询人工伙伴。 |
| 测试过于复杂 | 设计过于复杂,简化接口。 |
| 必须大量使用Mock | 代码耦合度过高,使用依赖注入。 |
| 测试准备工作繁重 | 提取辅助函数。仍然复杂?简化设计。 |
Debugging Integration
调试与TDD的结合
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
Never fix bugs without a test.
发现Bug?先写一个能复现Bug的失败测试,然后遵循TDD循环。测试既能验证修复效果,又能防止回归。
永远不要在没有测试的情况下修复Bug。
Testing Anti-Patterns
测试反模式
When adding mocks or test utilities, read Testing anti patterns to avoid common pitfalls:
- Testing mock behavior instead of real behavior
- Adding test-only methods to production classes
- Mocking without understanding dependencies
添加Mock或测试工具时,阅读Testing anti patterns避免常见陷阱:
- 测试Mock行为而非真实行为
- 为生产类添加仅用于测试的方法
- 不理解依赖关系就使用Mock
Final Rule
最终规则
Production code → test exists and failed first
Otherwise → not TDDNo exceptions without your human partner's permission.
Production code → test exists and failed first
Otherwise → not TDD除非获得人工伙伴的许可,否则无例外。