tdd-full-coverage
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTDD Full Coverage
TDD全代码覆盖率
Overview
概述
Test-Driven Development with full code coverage.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
Announce at start: "I'm using TDD to implement this feature."
具备全代码覆盖率的测试驱动开发(Test-Driven Development,TDD)。
核心原则: 如果你没看到测试失败,就无法确定它是否测试了正确的内容。
开始时声明: "我正在使用TDD来实现这个功能。"
The Iron Law
铁律
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRSTWrote code before a test? Delete it. Start over.
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST如果在写测试前就编写了生产代码,请删除它,重新开始。
Red-Green-Refactor Cycle
RED-GREEN-REFACTOR循环
┌─────────────────────────────────────────────┐
│ │
▼ │
┌───────┐ ┌───────┐ ┌──────────┐ │
│ RED │────►│ GREEN │────►│ REFACTOR │─────────┘
└───────┘ └───────┘ └──────────┘
Write Write Clean
failing minimal up code
test code (stay green) ┌─────────────────────────────────────────────┐
│ │
▼ │
┌───────┐ ┌───────┐ ┌──────────┐ │
│ RED │────►│ GREEN │────►│ REFACTOR │─────────┘
└───────┘ └───────┘ └──────────┘
Write Write Clean
failing minimal up code
test code (stay green)RED: Write Failing Test
RED:编写失败的测试
Write ONE test for ONE behavior.
typescript
// Test one specific thing
test('rejects empty email', async () => {
const result = await validateEmail('');
expect(result.valid).toBe(false);
expect(result.error).toBe('Email is required');
});为一种行为编写一个测试。
typescript
// Test one specific thing
test('rejects empty email', async () => {
const result = await validateEmail('');
expect(result.valid).toBe(false);
expect(result.error).toBe('Email is required');
});Verify RED: Watch It Fail
验证RED:观察测试失败
MANDATORY. Never skip.
bash
pnpm test --grep "rejects empty email"Confirm:
- Test FAILS (not errors)
- Fails for EXPECTED reason (feature missing, not typo)
- Error message is what you expect
If test passes → You're testing existing behavior. Fix the test.
必须执行,绝不能跳过。
bash
pnpm test --grep "rejects empty email"确认:
- 测试失败(不是报错)
- 失败原因符合预期(功能缺失,而非拼写错误)
- 错误消息与预期一致
如果测试通过→你正在测试已有的行为,请修改测试。
GREEN: Minimal Code
GREEN:编写最简代码
Write the SIMPLEST code to pass the test.
typescript
function validateEmail(email: string): ValidationResult {
if (!email) {
return { valid: false, error: 'Email is required' };
}
return { valid: true };
}Don't add:
- Error handling for cases you haven't tested
- Configuration options you don't need yet
- Optimizations
编写能通过测试的最简代码。
typescript
function validateEmail(email: string): ValidationResult {
if (!email) {
return { valid: false, error: 'Email is required' };
}
return { valid: true };
}请勿添加:
- 尚未测试的场景的错误处理
- 目前不需要的配置选项
- 优化代码
Verify GREEN: Watch It Pass
验证GREEN:观察测试通过
MANDATORY.
bash
pnpm test --grep "rejects empty email"Confirm:
- Test PASSES
- All other tests still pass
- No errors or warnings
必须执行。
bash
pnpm test --grep "rejects empty email"确认:
- 测试通过
- 所有其他测试仍能通过
- 无错误或警告
REFACTOR: Clean Up
REFACTOR:代码清理
After green, improve code quality:
- Remove duplication
- Improve names
- Extract helpers
Keep tests green during refactoring.
测试通过后,提升代码质量:
- 移除重复代码
- 优化命名
- 提取辅助函数
重构过程中需保持测试始终通过。
Repeat
重复循环
Write next failing test for next behavior.
为下一个行为编写新的失败测试,重复上述流程。
Coverage Requirements
覆盖率要求
Target: 100% for New Code
目标:新代码覆盖率100%
bash
undefinedbash
undefinedCheck coverage
检查覆盖率
pnpm test --coverage
pnpm test --coverage
Verify new code is covered
验证新代码覆盖率
Lines: 100%
分支覆盖率:100%
Branches: 100%
函数覆盖率:100%
Functions: 100%
行覆盖率:100%
Statements: 100%
语句覆盖率:100%
undefinedundefinedWhat 100% Means
100%覆盖率的含义
| Covered | Not Covered (Fix It) |
|---|---|
| All branches tested | Some if/else paths missed |
| All functions called | Unused functions |
| All error handlers triggered | Error paths untested |
| All edge cases verified | Only happy path |
| 已覆盖 | 未覆盖(需修复) |
|---|---|
| 所有分支均已测试 | 部分if/else路径未覆盖 |
| 所有函数均已调用 | 存在未使用的函数 |
| 所有错误处理均已触发 | 错误路径未测试 |
| 所有边界情况均已验证 | 仅测试了正常流程 |
Acceptable Exceptions
可接受的例外情况
These MAY have lower coverage (discuss with team):
- Configuration files
- Type definitions only
- Auto-generated code
- Third-party integration code (mock at boundary)
Document exceptions in coverage config:
javascript
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
coveragePathIgnorePatterns: [
'/node_modules/',
'/generated/',
'config.ts',
],
};以下情况覆盖率可低于100%(需与团队讨论):
- 配置文件
- 仅包含类型定义的文件
- 自动生成的代码
- 第三方集成代码(在边界处进行Mock)
在覆盖率配置文件中记录例外情况:
javascript
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
coveragePathIgnorePatterns: [
'/node_modules/',
'/generated/',
'config.ts',
],
};Integration Testing Against Local Services
针对本地服务的集成测试
Core principle: Unit tests with mocks are necessary but not sufficient. You MUST ALSO test against real services.
核心原则: 使用Mock的单元测试是必要的,但并不足够。你必须同时针对真实服务进行测试。
The Two-Layer Testing Requirement
双层测试要求
| Layer | Purpose | Uses Mocks? | Uses Real Services? |
|---|---|---|---|
| Unit Tests (TDD) | Verify logic, enable RED-GREEN-REFACTOR | YES | No |
| Integration Tests | Verify real service behavior | No | YES |
Both layers are REQUIRED. Unit tests alone miss real-world failures. Integration tests alone are too slow for TDD.
| 层级 | 目的 | 是否使用Mock | 是否使用真实服务 |
|---|---|---|---|
| 单元测试(TDD) | 验证逻辑,支持RED-GREEN-REFACTOR循环 | 是 | 否 |
| 集成测试 | 验证真实服务的行为 | 否 | 是 |
两层测试均为必填项。 仅单元测试会遗漏真实场景中的问题,仅集成测试对于TDD来说速度过慢。
The Problem We're Solving
我们要解决的问题
We've experienced 80% failure rates with ORM migrations because:
- Unit tests with mocks pass
- Real database rejects the migration
- CI discovers the bug instead of local testing
Mocks don't catch: Schema mismatches, constraint violations, migration failures, connection issues, transaction behavior.
我们曾遇到ORM迁移80%的失败率,原因如下:
- 使用Mock的单元测试通过
- 真实数据库拒绝迁移
- 问题在CI阶段才被发现,而非本地测试阶段
Mock无法捕获: Schema不匹配、约束冲突、迁移失败、连接问题、事务行为异常。
When Integration Tests Are Required
需要集成测试的场景
| Code Change | Unit Tests (with mocks) | Integration Tests (with real services) |
|---|---|---|
| Database model/migration | ✅ Required | ✅ Also required |
| Repository/ORM layer | ✅ Required | ✅ Also required |
| Cache operations | ✅ Required | ✅ Also required |
| Pub/sub messages | ✅ Required | ✅ Also required |
| Queue workers | ✅ Required | ✅ Also required |
| 代码变更 | 单元测试(使用Mock) | 集成测试(使用真实服务) |
|---|---|---|
| 数据库模型/迁移 | ✅ 必填 | ✅ 同样必填 |
| 仓库/ORM层 | ✅ 必填 | ✅ 同样必填 |
| 缓存操作 | ✅ 必填 | ✅ 同样必填 |
| 发布/订阅消息 | ✅ 必填 | ✅ 同样必填 |
| 队列工作器 | ✅ 必填 | ✅ 同样必填 |
Local Service Testing Protocol
本地服务测试流程
After completing TDD cycle (unit tests with mocks):
- Ensure services are running ()
docker-compose up -d - Run integration tests against real services
- Verify migrations apply ()
pnpm migrate - Verify in local environment before pushing
完成TDD循环(使用Mock的单元测试)后:
- 确保服务正在运行()
docker-compose up -d - 针对真实服务运行集成测试
- 验证迁移可正常执行()
pnpm migrate - 在推送前在本地环境验证
Example: Database Testing
示例:数据库测试
typescript
// LAYER 1: Unit tests with mocks (TDD cycle)
describe('UserRepository (unit)', () => {
const mockDb = { query: jest.fn() };
it('calls correct SQL for findById', async () => {
mockDb.query.mockResolvedValue([{ id: 1, email: 'test@example.com' }]);
const user = await userRepo.findById(1);
expect(mockDb.query).toHaveBeenCalledWith('SELECT * FROM users WHERE id = $1', [1]);
});
});
// LAYER 2: Integration tests with real postgres (ALSO required)
describe('UserRepository (integration)', () => {
beforeAll(async () => {
await db.migrate.latest();
});
it('actually persists and retrieves users', async () => {
await userRepo.create({ email: 'test@example.com' });
const user = await userRepo.findByEmail('test@example.com');
expect(user).toBeDefined();
expect(user.email).toBe('test@example.com');
});
it('enforces unique email constraint', async () => {
await userRepo.create({ email: 'unique@example.com' });
// Real postgres will throw - mocks won't catch this
await expect(
userRepo.create({ email: 'unique@example.com' })
).rejects.toThrow(/unique constraint/);
});
});Skill:
local-service-testingtypescript
// 层级1:使用Mock的单元测试(TDD循环)
describe('UserRepository (unit)', () => {
const mockDb = { query: jest.fn() };
it('calls correct SQL for findById', async () => {
mockDb.query.mockResolvedValue([{ id: 1, email: 'test@example.com' }]);
const user = await userRepo.findById(1);
expect(mockDb.query).toHaveBeenCalledWith('SELECT * FROM users WHERE id = $1', [1]);
});
});
// 层级2:针对真实Postgres的集成测试(同样必填)
describe('UserRepository (integration)', () => {
beforeAll(async () => {
await db.migrate.latest();
});
it('actually persists and retrieves users', async () => {
await userRepo.create({ email: 'test@example.com' });
const user = await userRepo.findByEmail('test@example.com');
expect(user).toBeDefined();
expect(user.email).toBe('test@example.com');
});
it('enforces unique email constraint', async () => {
await userRepo.create({ email: 'unique@example.com' });
// 真实Postgres会抛出异常 - Mock无法捕获该问题
await expect(
userRepo.create({ email: 'unique@example.com' })
).rejects.toThrow(/unique constraint/);
});
});Skill:
local-service-testingTest Quality
测试质量
Good Tests
优质测试
typescript
// GOOD: Clear name, tests one thing
test('calculates tax for positive amount', () => {
const result = calculateTax(100, 0.08);
expect(result).toBe(8);
});
test('returns zero tax for zero amount', () => {
const result = calculateTax(0, 0.08);
expect(result).toBe(0);
});
test('throws for negative amount', () => {
expect(() => calculateTax(-100, 0.08)).toThrow('Amount must be positive');
});typescript
// 优质:名称清晰,仅测试单一内容
test('calculates tax for positive amount', () => {
const result = calculateTax(100, 0.08);
expect(result).toBe(8);
});
test('returns zero tax for zero amount', () => {
const result = calculateTax(0, 0.08);
expect(result).toBe(0);
});
test('throws for negative amount', () => {
expect(() => calculateTax(-100, 0.08)).toThrow('Amount must be positive');
});Bad Tests
劣质测试
typescript
// BAD: Tests multiple things
test('calculateTax works', () => {
expect(calculateTax(100, 0.08)).toBe(8);
expect(calculateTax(0, 0.08)).toBe(0);
expect(() => calculateTax(-100, 0.08)).toThrow();
});
// BAD: Tests mock, not real code
test('calls the tax service', () => {
const mockTaxService = jest.fn().mockReturnValue(8);
const result = calculateTax(100, 0.08);
expect(mockTaxService).toHaveBeenCalled(); // Testing mock, not behavior
});typescript
// 劣质:测试多个内容
test('calculateTax works', () => {
expect(calculateTax(100, 0.08)).toBe(8);
expect(calculateTax(0, 0.08)).toBe(0);
expect(() => calculateTax(-100, 0.08)).toThrow();
});
// 劣质:测试Mock而非真实代码
test('calls the tax service', () => {
const mockTaxService = jest.fn().mockReturnValue(8);
const result = calculateTax(100, 0.08);
expect(mockTaxService).toHaveBeenCalled(); // 测试Mock而非行为
});Testing Patterns
测试模式
Arrange-Act-Assert
准备-执行-断言(Arrange-Act-Assert)
typescript
test('description', () => {
// Arrange - set up test data
const user = createTestUser({ email: 'test@example.com' });
const input = { userId: user.id, action: 'update' };
// Act - perform the action
const result = processAction(input);
// Assert - verify the outcome
expect(result.success).toBe(true);
expect(result.timestamp).toBeDefined();
});typescript
test('description', () => {
// Arrange - 设置测试数据
const user = createTestUser({ email: 'test@example.com' });
const input = { userId: user.id, action: 'update' };
// Act - 执行操作
const result = processAction(input);
// Assert - 验证结果
expect(result.success).toBe(true);
expect(result.timestamp).toBeDefined();
});Testing Errors
错误测试
typescript
test('throws for invalid input', () => {
expect(() => validateInput(null)).toThrow(ValidationError);
expect(() => validateInput(null)).toThrow('Input is required');
});
test('async throws for invalid input', async () => {
await expect(asyncValidate(null)).rejects.toThrow(ValidationError);
});typescript
test('throws for invalid input', () => {
expect(() => validateInput(null)).toThrow(ValidationError);
expect(() => validateInput(null)).toThrow('Input is required');
});
test('async throws for invalid input', async () => {
await expect(asyncValidate(null)).rejects.toThrow(ValidationError);
});Testing Side Effects
副作用测试
typescript
test('logs error on failure', async () => {
const logSpy = jest.spyOn(logger, 'error');
await processWithFailure();
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to process')
);
});typescript
test('logs error on failure', async () => {
const logSpy = jest.spyOn(logger, 'error');
await processWithFailure();
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to process')
);
});Mocking Guidelines
Mocking指南
When to Mock
何时使用Mock
| Mock | Don't Mock |
|---|---|
| External APIs | Your own code |
| Database (integration) | Simple functions |
| File system | Pure logic |
| Time/dates | Deterministic code |
| Network requests | Internal modules |
| 可Mock | 不可Mock |
|---|---|
| 外部API | 自有代码 |
| 数据库(集成测试中) | 简单函数 |
| 文件系统 | 纯逻辑代码 |
| 时间/日期 | 确定性代码 |
| 网络请求 | 内部模块 |
Mock at Boundaries
在边界处Mock
typescript
// GOOD: Mock the external boundary
const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ data: 'test' }))
);
// BAD: Mock internal implementation
const internalMock = jest.spyOn(utils, 'internalHelper');typescript
// 优质:Mock外部边界
const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ data: 'test' }))
);
// 劣质:Mock内部实现
const internalMock = jest.spyOn(utils, 'internalHelper');Debugging Test Failures
调试测试失败
| Problem | Solution |
|---|---|
| Test passes when should fail | Check assertion (expect syntax) |
| Test fails unexpectedly | Check test isolation (cleanup) |
| Flaky tests | Remove timing dependencies |
| Hard to test | Improve code design |
| 问题 | 解决方案 |
|---|---|
| 本该失败的测试通过 | 检查断言(expect语法) |
| 测试意外失败 | 检查测试隔离(清理工作) |
| 不稳定测试 | 移除时间依赖 |
| 难以测试 | 优化代码设计 |
Checklist
检查清单
Before completing a feature:
- Every function has at least one test
- Watched each test fail before implementing
- Each failure was for expected reason
- Wrote minimal code to pass
- All tests pass
- Coverage is 100% for new code
- No skipped tests
- Tests are isolated (no order dependency)
- Error cases are tested
- Integration tests ran against local services (not mocks)
- All service-dependent code verified locally
完成功能前:
- 每个函数至少有一个测试
- 每个测试在实现前都观察到失败
- 每次失败的原因均符合预期
- 编写了通过测试的最简代码
- 所有测试均通过
- 新代码覆盖率为100%
- 无跳过的测试
- 测试相互隔离(无顺序依赖)
- 错误场景均已测试
- 针对本地服务运行了集成测试(未使用Mock)
- 所有依赖服务的代码均已在本地验证
Integration
集成
This skill is called by:
- - Step 7, 8, 11
issue-driven-development
This skill uses:
- - Tests should be typed
strict-typing - - Document test utilities
inline-documentation
This skill ensures:
- Verified behavior
- Regression prevention
- Refactoring safety
- Documentation through tests
本技能被以下技能调用:
- - 步骤7、8、11
issue-driven-development
本技能使用以下技能:
- - 测试应包含类型定义
strict-typing - - 为测试工具编写文档
inline-documentation
本技能可确保:
- 行为已验证
- 防止回归问题
- 重构安全性
- 通过测试实现文档化