tdd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
Mode: Cognitive/Prompt-Driven — No standalone utility script; use via agent context.
模式:认知/提示驱动 — 无独立实用脚本;通过Agent上下文使用。

Test-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 FIRST
Write 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

红-绿-重构循环

RED - Write Failing Test

红 - 编写失败的测试

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

验证红阶段 - 观察测试失败

MANDATORY. Never skip.
bash
npm test path/to/test.test.ts
Confirm:
  • 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

绿 - 编写最小化代码

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

验证绿阶段 - 观察测试通过

MANDATORY.
bash
npm test path/to/test.test.ts
Confirm:
  • 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

重构 - 代码清理

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

优秀测试的标准

QualityGoodBad
MinimalOne thing. "and" in name? Split it.
test('validates email and domain and whitespace')
ClearName describes behavior
test('test1')
Shows intentDemonstrates desired APIObscures what code should do
质量维度良好实践不良实践
最小化仅测试一项功能。名称里有“和”?拆分测试。
test('validates email and domain and whitespace')
清晰性名称描述行为
test('test1')
体现意图展示期望的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" does not equal 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 does not equal TDD. You get coverage, lose proof tests work.
“我会先写代码,之后再写测试验证功能”
在代码之后写的测试会立即通过。立即通过的测试无法证明任何事:
  • 可能测试了错误的内容
  • 可能测试的是实现细节而非行为
  • 可能遗漏了你忘记的边缘情况
  • 你从未看到它捕获Bug
先写测试会迫使你看到测试失败,证明它确实测试了某项功能。
“我已经手动测试了所有边缘情况”
手动测试是临时的。你以为测试了所有情况,但:
  • 没有记录测试了哪些内容
  • 代码变更后无法重新运行测试
  • 压力下容易忘记测试用例
  • “我试的时候是好的”并不等于全面测试
自动化测试是系统化的,每次运行的方式都一致。
“删除X小时的工作是浪费”
这是沉没成本谬误。时间已经花了。你现在的选择是:
  • 删除代码,用TDD重写(再花X小时,高可信度)
  • 保留代码,之后添加测试(30分钟,低可信度,可能存在Bug)
真正的“浪费”是保留你无法信任的代码。没有真实测试的可运行代码是技术债务。
“TDD太教条,务实意味着灵活应变”
TDD才是务实的:
  • 提交前发现Bug(比提交后调试更快)
  • 防止回归(测试立即捕获代码断裂)
  • 记录行为(测试展示如何使用代码)
  • 支持重构(自由修改代码,测试捕获断裂)
“务实”的捷径 = 生产环境调试 = 更慢。
“之后写测试能达到同样的目标——重要的是精神而非形式”
并非如此。之后写的测试回答“这段代码做了什么?” 先写的测试回答“这段代码应该做什么?”
之后写的测试会受实现方式的偏见影响。你测试的是你构建的内容,而非需求。你验证的是你记得的边缘情况,而非发现的新情况。
先写测试会迫使你在实现前发现边缘情况。之后写测试只能验证你是否记得所有情况(你肯定会遗漏)。
花30分钟之后写测试不等于TDD。你得到了覆盖率,但失去了测试有效的证明。

Common Rationalizations

常见的合理化借口

ExcuseReality
"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 does not equal 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 undefined
GREEN
typescript
function submitForm(data: FormData) {
  if (!data.email?.trim()) {
    return { error: 'Email required' };
  }
  // ...
}
Verify GREEN
bash
$ npm test
PASS
REFACTOR Extract validation for multiple fields if needed.
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
重构 如果需要,提取多字段验证逻辑。

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
  • pnpm lint:fix
    passes with 0 errors
  • pnpm format
    produces no changes
  • 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.
标记工作完成前:
  • 每个新函数/方法都有测试
  • 实现前观察每个测试失败
  • 每个测试因预期原因失败(功能缺失,不是拼写错误)
  • 编写最少的代码通过每个测试
  • 所有测试通过
  • pnpm lint:fix
    执行通过,0错误
  • pnpm format
    执行后无代码变更
  • 输出干净(无错误、警告)
  • 测试使用真实代码(万不得已才用Mock)
  • 覆盖边缘情况和错误场景
无法勾选所有选项?你跳过了TDD。重新开始。

Pre-Completion: Lint and Format (MANDATORY)

完成前必须:代码检查与格式化(强制要求)

Before marking any task complete, run:
  1. pnpm lint:fix
    — fix all linting issues
  2. pnpm format
    — format all files
  3. If either command fails, fix the issues before proceeding
This is a BLOCKING requirement. Tasks are NOT complete until both pass.
标记任何任务完成前,运行:
  1. pnpm lint:fix
    — 修复所有代码检查问题
  2. pnpm format
    — 格式化所有文件
  3. 如果任一命令失败,修复问题后再继续
这是阻塞性要求。 只有两个命令都通过,任务才算完成。

When Stuck

遇到困境时

ProblemSolution
Don't know how to testWrite wished-for API. Write assertion first. Ask your human partner.
Test too complicatedDesign too complicated. Simplify interface.
Must mock everythingCode too coupled. Use dependency injection.
Test setup hugeExtract helpers. Still complex? Simplify design.
问题解决方案
不知道如何测试编写期望的API。先写断言。咨询你的人类合作者。
测试过于复杂设计过于复杂。简化接口。
必须Mock所有依赖代码耦合度太高。使用依赖注入。
测试准备工作繁重提取辅助函数。仍然复杂?简化设计。

Debugging Integration

调试集成

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.md 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.md以避免常见陷阱:
  • 测试Mock行为而非真实行为
  • 为生产类添加仅用于测试的方法
  • 不理解依赖就使用Mock

Final Rule

最终规则

Production code -> test exists and failed first
Otherwise -> not TDD
No exceptions without your human partner's permission.
Production code -> test exists and failed first
Otherwise -> not TDD
未经人类合作者许可,无例外。

Framework-Specific Testing Guidelines

框架特定测试指南

This section consolidates testing patterns from the
testing-expert
skill.
本节整合了
testing-expert
技能中的测试模式。

Angular Testing

Angular测试

FrameworkPurposeUsage
Jasmine + KarmaUnit testing components and servicesDefault Angular test runner
Angular Testing UtilitiesComponent testing with focus on user behaviorDOM interaction testing
Protractor / CypressEnd-to-end testing of user flowsFull integration tests
Best Practices:
  • Implement proper test coverage targets (aim for 80%+ coverage)
  • Use mocking for external dependencies and HTTP calls in unit tests
  • Test both success and error states for components and services
  • Use snapshot testing sparingly and only for simple, stable components
框架用途使用场景
Jasmine + Karma组件和服务的单元测试Angular默认测试运行器
Angular Testing Utilities聚焦用户行为的组件测试DOM交互测试
Protractor / Cypress用户流程的端到端测试全集成测试
最佳实践:
  • 设定合理的测试覆盖率目标(目标80%+)
  • 单元测试中为外部依赖和HTTP调用使用Mock
  • 测试组件和服务的成功与错误状态
  • 谨慎使用快照测试,仅用于简单、稳定的组件

Test Type Selection

测试类型选择

Test TypeWhen to UseTools
Unit TestsIsolated component/function logicJest, Vitest, pytest
Integration TestsMultiple components working togetherTesting Library, pytest
E2E TestsFull user flows through the applicationCypress, Playwright
API TestsEndpoint validationSupertest, httpx
测试类型使用场景工具
单元测试孤立的组件/函数逻辑测试Jest, Vitest, pytest
集成测试多个组件协作的测试Testing Library, pytest
端到端测试应用全用户流程测试Cypress, Playwright
API测试端点验证Supertest, httpx

Testing and Monitoring

测试与监控

  • Guide the creation of test cases for each component
  • Assist with setting up monitoring and logging
  • Help interpret performance metrics and suggest optimizations
  • Prioritize security, scalability, and maintainability
  • 指导为每个组件创建测试用例
  • 协助设置监控与日志
  • 帮助解读性能指标并提出优化建议
  • 优先考虑安全性、可扩展性和可维护性

Related Skills

相关技能

  • test-generator
    - Generates test code from specifications
  • comprehensive-unit-testing-with-pytest
    - Python-specific pytest patterns
  • qa-workflow
    - Systematic QA validation with fix loops
  • test-generator
    - 根据规格生成测试代码
  • comprehensive-unit-testing-with-pytest
    - Python特定的pytest模式
  • qa-workflow
    - 系统化的QA验证与修复循环

Memory Protocol (MANDATORY)

记忆协议(强制要求)

Before starting: Read
.claude/context/memory/learnings.md
After completing:
  • New pattern ->
    .claude/context/memory/learnings.md
  • Issue found ->
    .claude/context/memory/issues.md
  • Decision made ->
    .claude/context/memory/decisions.md
ASSUME INTERRUPTION: If it's not in memory, it didn't happen.
开始前: 阅读
.claude/context/memory/learnings.md
完成后:
  • 新模式 -> 写入
    .claude/context/memory/learnings.md
  • 发现的问题 -> 写入
    .claude/context/memory/issues.md
  • 做出的决策 -> 写入
    .claude/context/memory/decisions.md
假设会被中断:如果没记录在记忆中,就等于没发生。