tdd-full-coverage

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TDD 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 FIRST
Wrote 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
undefined
bash
undefined

Check coverage

检查覆盖率

pnpm test --coverage
pnpm test --coverage

Verify new code is covered

验证新代码覆盖率

Lines: 100%

分支覆盖率:100%

Branches: 100%

函数覆盖率:100%

Functions: 100%

行覆盖率:100%

Statements: 100%

语句覆盖率:100%

undefined
undefined

What 100% Means

100%覆盖率的含义

CoveredNot Covered (Fix It)
All branches testedSome if/else paths missed
All functions calledUnused functions
All error handlers triggeredError paths untested
All edge cases verifiedOnly 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

双层测试要求

LayerPurposeUses Mocks?Uses Real Services?
Unit Tests (TDD)Verify logic, enable RED-GREEN-REFACTORYESNo
Integration TestsVerify real service behaviorNoYES
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 ChangeUnit Tests (with mocks)Integration Tests (with real services)
Database model/migration✅ RequiredAlso required
Repository/ORM layer✅ RequiredAlso required
Cache operations✅ RequiredAlso required
Pub/sub messages✅ RequiredAlso required
Queue workers✅ RequiredAlso required
代码变更单元测试(使用Mock)集成测试(使用真实服务)
数据库模型/迁移✅ 必填同样必填
仓库/ORM层✅ 必填同样必填
缓存操作✅ 必填同样必填
发布/订阅消息✅ 必填同样必填
队列工作器✅ 必填同样必填

Local Service Testing Protocol

本地服务测试流程

After completing TDD cycle (unit tests with mocks):
  1. Ensure services are running (
    docker-compose up -d
    )
  2. Run integration tests against real services
  3. Verify migrations apply (
    pnpm migrate
    )
  4. Verify in local environment before pushing
完成TDD循环(使用Mock的单元测试)后:
  1. 确保服务正在运行
    docker-compose up -d
  2. 针对真实服务运行集成测试
  3. 验证迁移可正常执行
    pnpm migrate
  4. 在推送前在本地环境验证

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-testing
typescript
// 层级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-testing

Test 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

MockDon't Mock
External APIsYour own code
Database (integration)Simple functions
File systemPure logic
Time/datesDeterministic code
Network requestsInternal 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

调试测试失败

ProblemSolution
Test passes when should failCheck assertion (expect syntax)
Test fails unexpectedlyCheck test isolation (cleanup)
Flaky testsRemove timing dependencies
Hard to testImprove 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:
  • issue-driven-development
    - Step 7, 8, 11
This skill uses:
  • strict-typing
    - Tests should be typed
  • inline-documentation
    - Document test utilities
This skill ensures:
  • Verified behavior
  • Regression prevention
  • Refactoring safety
  • Documentation through tests
本技能被以下技能调用:
  • issue-driven-development
    - 步骤7、8、11
本技能使用以下技能:
  • strict-typing
    - 测试应包含类型定义
  • inline-documentation
    - 为测试工具编写文档
本技能可确保:
  • 行为已验证
  • 防止回归问题
  • 重构安全性
  • 通过测试实现文档化