test-driven-development

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Test-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 FIRST
Wrote 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.ts
Test 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.ts
Confirm: 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 undefined
GREEN:
typescript
function submitForm(data: FormData) {
  if (!data.email?.trim()) {
    return { error: 'Email required' };
  }
  // ...
}
Verify GREEN:
bash
$ npm test
PASS
REFACTOR: 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

遇到问题时

ProblemSolution
Don't know how to testWrite the API you wish existed. Write assertion first.
Test too complicatedDesign too complicated. Simplify the interface.
Must mock everythingCode too coupled. Introduce dependency injection.
Test setup hugeExtract 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:
  1. Write tests that capture current behavior (even if "wrong")
  2. Run tests, observe actual outputs
  3. Update assertions to match reality (these are "golden masters")
  4. Now you have a safety net for refactoring
  5. 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.
铁律(“删除并重新开始”)仅适用于你自己没写测试就新增的代码。对于没有测试的继承代码,使用特征测试:
  1. 编写测试捕获当前行为(哪怕这个行为是“错误”的)
  2. 运行测试,观察实际输出
  3. 更新断言匹配实际结果(这些就是“黄金基准”)
  4. 现在你就有了重构的安全网
  5. 新的行为变更使用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 TDD
Production code exists → test existed first and failed first
Otherwise → not TDD