test-driven-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTest-Driven Development
测试驱动开发(Test-Driven Development)
Write test first. Watch it fail. Write minimal code to pass. Refactor.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
先写测试,观察它失败,编写最少的可通过测试的代码,再进行重构。
核心原则: 如果你没有亲眼看到测试失败,你就无法确定它是否测试了正确的内容。
The Iron Law
铁律
NO BEHAVIOR-CHANGING PRODUCTION CODE WITHOUT A FAILING TEST FIRSTWrote code before test? Delete it completely. Implement fresh from tests.
Refactoring is exempt: The refactor step changes structure, not behavior. Tests stay green throughout. No new failing test required.
NO BEHAVIOR-CHANGING PRODUCTION CODE WITHOUT A FAILING TEST FIRST在写测试之前就写了代码?请完全删除它,从测试开始重新实现。
重构例外: 重构步骤仅变更代码结构,不改变行为,整个过程中测试应保持正常通过,不需要新增失败的测试。
Red-Green-Refactor Cycle
红-绿-重构周期
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红 ──► 验证失败 ──► 绿 ──► 验证通过 ──► 重构 ──► 验证通过 ──► 下一个红
│ │ │
▼ ▼ ▼
失败原因错误? 仍未通过? 破坏了现有测试?
修复测试后重试 修复代码后重试 修复后重试RED - Write Failing Test
红 - 编写失败的测试
Write one minimal test for one behavior.
Good example:
typescript
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);
});Clear name, tests real behavior, asserts observable outcome
Bad example:
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, asserts only call count without verifying outcome, tests mock mechanics not behavior
Requirements: One behavior. Clear name. Real code (mocks only if unavoidable).
为单个行为编写一个最小化的测试。
正面示例:
typescript
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);
});命名清晰,测试真实行为,断言可观测的输出结果
反面示例:
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机制而非真实行为
要求: 单测试对应单行为,命名清晰,使用真实代码(仅在不可避免时使用mock)。
Verify RED - Watch It Fail
验证红 - 观察测试失败
MANDATORY. Never skip.
bash
npm test path/to/test.test.tsTest must go red for the right reason. Acceptable RED states:
- Assertion failure (expected behavior missing)
- Compile/type error (function doesn't exist yet)
Not acceptable: Runtime setup errors, import failures, environment issues.
Test passes immediately? You're testing existing behavior—fix test.
Test errors for wrong reason? Fix error, re-run until it fails correctly.
强制要求,绝对不能跳过。
bash
npm test path/to/test.test.ts测试必须因为正确的原因变红,可接受的红状态包括:
- 断言失败(缺少预期的行为)
- 编译/类型错误(对应的函数还不存在)
不可接受的原因:运行时设置错误、导入失败、环境问题。
测试直接通过了?说明你测试的是已存在的行为——请修复测试。
测试因为错误的原因报错?修复错误后重新运行,直到它正确失败。
GREEN - Minimal Code
绿 - 编写最小化代码
Write simplest code to pass the test.
Good example:
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
Bad example:
typescript
async function retryOperation<T>(
fn: () => Promise<T>,
options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; }
): Promise<T> { /* YAGNI */ }Over-engineered beyond test requirements
Write only what the test demands. No extra features, no "improvements."
编写最简单的可通过测试的代码。
正面示例:
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');
}刚好满足通过测试的要求
反面示例:
typescript
async function retryOperation<T>(
fn: () => Promise<T>,
options?: { maxRetries?: number; backoff?: 'linear' | 'exponential'; }
): Promise<T> { /* YAGNI */ }过度设计,超出了当前测试的要求
只写测试要求的内容,不要加额外功能,不要做“提前优化”。
Verify GREEN - Watch It Pass
验证绿 - 观察测试通过
MANDATORY.
bash
npm test path/to/test.test.tsConfirm: Test passes. All other tests still pass. Output pristine (no errors, warnings).
Test fails? Fix code, not test.
Other tests fail? Fix now before continuing.
强制要求。
bash
npm test path/to/test.test.ts确认:当前测试通过,所有其他测试仍正常通过,输出干净(无错误、无警告)。
测试失败?修复代码,不要修改测试。
其他测试失败?在继续下一步前先修复问题。
REFACTOR - Clean Up
重构 - 清理优化
After green only: Remove duplication. Improve names. Extract helpers.
Keep tests green throughout. Add no new behavior.
仅在测试全绿后执行:移除重复代码,优化命名,提取辅助函数。
整个过程中保持测试全绿,不要新增任何行为。
Repeat
重复
Next failing test for next behavior.
为下一个行为编写下一个失败的测试,重复上述流程。
Good Tests
优质测试的标准
Minimal: One thing per test. "and" in name? Split it. ❌
test('validates email and domain and whitespace')Clear: Name describes behavior. ❌
test('test1')Shows intent: Demonstrates desired API usage, not implementation details.
最小化: 单测试仅测一件事,命名里出现“和”?拆分它。❌
test('validates email and domain and whitespace')清晰: 命名描述测试的行为。❌
test('test1')表意明确: 展示期望的API用法,而非实现细节。
Example: Bug Fix
示例:漏洞修复
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 helper if pattern repeats.
漏洞: 空邮箱被允许提交
红阶段:
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重构: 如果相同模式重复出现,可提取校验辅助函数。
Red Flags - STOP and Start Over
危险信号 - 停止并重新开始
Any of these means delete code and restart with TDD:
- Code written before test
- Test passes immediately (testing existing behavior)
- Can't explain why test failed
- Rationalizing "just this once" or "this is different"
- Keeping code "as reference" while writing tests
- Claiming "tests after achieve the same purpose"
出现以下任意情况,请删除现有代码,用TDD重新开始:
- 测试前就写了代码
- 测试直接通过(测试的是已存在的行为)
- 无法解释测试失败的原因
- 为自己找借口“就这一次例外”或者“这个场景不一样”
- 写测试的时候保留原来的代码“作为参考”
- 声称“写完再补测试效果一样”
When Stuck
遇到问题时
| Problem | Solution |
|---|---|
| Don't know how to test | Write the API you wish existed. Write assertion first. |
| Test too complicated | Design too complicated. Simplify the interface. |
| Must mock everything | Code too coupled. Introduce dependency injection. |
| Test setup huge | Extract helpers. Still complex? Simplify design. |
| 问题 | 解决方案 |
|---|---|
| 不知道怎么写测试 | 先写出你期望存在的API,先写断言 |
| 测试太复杂 | 说明设计太复杂,简化接口 |
| 必须mock所有依赖 | 说明代码耦合度太高,引入依赖注入 |
| 测试设置代码非常多 | 提取辅助函数,如果还是复杂就简化设计 |
Legacy Code (No Existing Tests)
遗留代码(无现有测试)
The Iron Law ("delete and restart") applies to new code you wrote without tests. For inherited code with no tests, use characterization tests:
- Write tests that capture current behavior (even if "wrong")
- Run tests, observe actual outputs
- Update assertions to match reality (these are "golden masters")
- Now you have a safety net for refactoring
- Apply TDD for new behavior changes
Characterization tests lock down existing behavior so you can refactor safely. They're the on-ramp, not a permanent state.
铁律(“删除并重新开始”)仅适用于你自己没写测试就新增的代码。对于没有测试的继承代码,使用特征测试:
- 编写测试捕获当前行为(哪怕这个行为是“错误”的)
- 运行测试,观察实际输出
- 更新断言匹配实际结果(这些就是“黄金基准”)
- 现在你就有了重构的安全网
- 新的行为变更使用TDD实现
特征测试锁定了现有行为,让你可以安全重构,它是接入TDD的过渡手段,不是永久状态。
Flakiness Rules
不稳定性规则
Tests must be deterministic. Ban these in unit tests:
- Real sleeps / delays → Use fake timers (,
vi.useFakeTimers())jest.useFakeTimers() - Wall clock time → Inject clock, assert against injected time
- Math.random() → Seed or inject RNG
- Network calls → Mock at boundary or use MSW
- Filesystem race conditions → Use temp dirs with unique names
Flaky test? Fix or delete. Flaky tests erode trust in the entire suite.
测试必须是确定性的,单元测试中禁止使用以下内容:
- 真实sleep/延迟 → 使用模拟计时器(、
vi.useFakeTimers())jest.useFakeTimers() - 系统时钟时间 → 注入时钟实例,基于注入的时间做断言
- Math.random() → 固定随机种子或注入随机数生成器
- 网络请求 → 在边界层mock或使用MSW
- 文件系统竞态条件 → 使用名称唯一的临时目录
测试不稳定?要么修复它要么删除它,不稳定的测试会侵蚀对整个测试套件的信任。
Debugging Integration
调试集成
Bug found? Write failing test reproducing it first. Then follow TDD cycle. Test proves fix and prevents regression.
发现bug?先编写可复现bug的失败测试,再遵循TDD周期处理,测试可以证明修复有效,还能避免回归。
Planning: Test List
规划:测试清单
Before diving into the cycle, spend 2 minutes listing the next 3-10 tests you expect to write. This prevents local-optimum design where early tests paint you into a corner.
Example test list for a retry function:
- retries N times on failure
- returns result on success
- throws after max retries exhausted
- calls onRetry callback between attempts
- respects backoff delay
Work through the list in order. Add/remove tests as you learn.
在进入开发周期前,花2分钟列出你接下来要写的3-10个测试,这可以避免局部最优的设计,防止早期的测试把你逼进死角。
重试函数的测试清单示例:
- 失败时重试N次
- 成功时直接返回结果
- 达到最大重试次数后抛出错误
- 两次尝试之间调用onRetry回调
- 遵循退避延迟配置
按顺序处理清单,过程中可以根据认知新增/移除测试。
Testing Anti-Patterns
测试反模式
When writing tests involving mocks, dependencies, or test utilities: See references/testing-anti-patterns.md for common pitfalls including testing mock behavior and adding test-only methods to production classes.
编写涉及mock、依赖或测试工具的测试时:参考references/testing-anti-patterns.md了解常见陷阱,包括测试mock行为、给生产类加仅测试用的方法等。
Philosophy and Rationalizations
理念与异议回应
For detailed rebuttals to common objections ("I'll test after", "deleting work is wasteful", "TDD is dogmatic"): See references/tdd-philosophy.md
对常见异议的详细反驳(“我写完再补测试”、“删除代码是浪费”、“TDD太教条”):参考references/tdd-philosophy.md
Final Rule
最终规则
Production code exists → test existed first and failed first
Otherwise → not TDDProduction code exists → test existed first and failed first
Otherwise → not TDD