tdd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TDD Implementation Skill

TDD 实现实操指南

Enforce a strict test-driven development cycle: Red → Green → Refactor → Commit.
遵循严格的测试驱动开发周期:红 → 绿 → 重构 → 提交。

Steps

步骤

1. Understand the Requirement

1. 理解需求

Read the task description carefully. Identify:
  • What behavior needs to exist (inputs, outputs, side effects)
  • Where it belongs in the codebase (which workspace, which module)
  • Acceptance criteria (what "done" looks like)
If the requirement is ambiguous, ask the user for clarification before writing any code.
仔细阅读任务描述,明确:
  • 要实现什么:需要具备的行为(输入、输出、副作用)
  • 代码位置:在代码库中的归属(哪个工作区、哪个模块)
  • 验收标准:完成的定义是什么
如果需求不明确,在编写任何代码前先向用户确认。

2. Find the Right Test File

2. 找到正确的测试文件

Locate or create the test file following project conventions:
  • Colocated tests:
    src/foo.ts
    src/foo.test.ts
  • Test directory:
    src/routes/bar.ts
    src/tests/routes/bar.test.ts
  • Monorepo: Check each workspace's vitest/jest config for test file patterns
If no test file exists, create one with proper imports and
describe()
block.
按照项目规范定位或创建测试文件:
  • 同目录测试
    src/foo.ts
    src/foo.test.ts
  • 测试目录
    src/routes/bar.ts
    src/tests/routes/bar.test.ts
  • 单仓库多项目(Monorepo):检查每个工作区的vitest/jest配置中的测试文件规则
如果测试文件不存在,创建包含正确导入和
describe()
块的测试文件。

3. Verify Mock Setup (Bootstrap Phase)

3. 验证模拟配置(初始化阶段)

Before writing any tests, validate that your mocks will work correctly:
  1. Check test runner config for
    mockReset
    /
    mockClear
    /
    restoreMocks
    settings:
bash
grep -rn 'mockReset\|mockClear\|restoreMocks' vitest.config.* jest.config.*
If
mockReset: true
is set, mocks are reset between tests — you MUST reconfigure return values in
beforeEach
, not at module scope.
  1. Write ONE minimal test that imports the module under test and verifies mocks resolve correctly:
typescript
import { describe, it, expect, vi, beforeEach } from "vitest";

const mocks = vi.hoisted(() => ({
  myDep: vi.fn(),
}));

vi.mock("../path/to/dependency", () => ({
  myDep: (...args: unknown[]) => mocks.myDep(...args),
}));

describe("bootstrap", () => {
  beforeEach(() => {
    mocks.myDep.mockResolvedValue({ ok: true });
  });

  it("mock resolves correctly", async () => {
    const { myDep } = await import("../path/to/dependency");
    expect(await myDep()).toEqual({ ok: true });
  });
});
  1. Run it and confirm it passes:
bash
npx vitest run <test-file> --reporter=verbose
  1. Only then proceed to write the full test suite following that proven mock pattern.
Why this matters:
vi.hoisted()
moves mock declarations above
vi.mock()
hoisting, avoiding temporal dead zone issues. Validating one mock round-trip before writing 20 tests saves significant debugging time.
在编写任何测试前,先验证你的模拟对象能否正常工作:
  1. 检查测试运行器配置中的
    mockReset
    /
    mockClear
    /
    restoreMocks
    设置:
bash
grep -rn 'mockReset\|mockClear\|restoreMocks' vitest.config.* jest.config.*
如果设置了
mockReset: true
,测试之间会重置模拟对象——你必须在
beforeEach
中重新配置返回值,而不是在模块作用域中配置。
  1. 编写一个最简测试用例,导入待测试模块并验证模拟对象能正确解析:
typescript
import { describe, it, expect, vi, beforeEach } from "vitest";

const mocks = vi.hoisted(() => ({
  myDep: vi.fn(),
}));

vi.mock("../path/to/dependency", () => ({
  myDep: (...args: unknown[]) => mocks.myDep(...args),
}));

describe("bootstrap", () => {
  beforeEach(() => {
    mocks.myDep.mockResolvedValue({ ok: true });
  });

  it("mock resolves correctly", async () => {
    const { myDep } = await import("../path/to/dependency");
    expect(await myDep()).toEqual({ ok: true });
  });
});
  1. 运行测试并确认通过:
bash
npx vitest run <test-file> --reporter=verbose
  1. 只有通过后,再按照已验证的模拟模式编写完整测试套件。
为什么这很重要
vi.hoisted()
会将模拟声明移到
vi.mock()
提升之前,避免暂时性死区问题。在编写20个测试前先验证一次模拟往返,能节省大量调试时间。

4. Write Failing Tests FIRST (Red Phase)

4. 先编写失败的测试用例(红阶段)

Remove the bootstrap test once you've confirmed the mock pattern works.
Write test cases that describe the expected behavior. Include:
  • Happy path: Normal operation with valid inputs
  • Edge cases: Empty inputs, boundary values, missing optional fields
  • Error cases: Invalid inputs, unauthorized access, missing resources
bash
npx vitest run <test-file> --reporter=verbose
Checkpoint: All new tests MUST fail. If any pass, the tests are not testing new behavior — revise them.
确认模拟模式可行后,移除初始化测试用例。
编写描述预期行为的测试用例,包括:
  • 正常路径:使用有效输入的常规操作
  • 边界情况:空输入、边界值、缺失可选字段
  • 错误场景:无效输入、未授权访问、缺失资源
bash
npx vitest run <test-file> --reporter=verbose
检查点:所有新测试必须失败。如果有测试通过,说明该测试没有测试新行为——需要修改测试用例。

5. Implement Minimum Code (Green Phase)

5. 实现最小化代码(绿阶段)

Write the minimum code to make all tests pass. Do NOT:
  • Add features not covered by tests
  • Optimize prematurely
  • Add error handling for untested scenarios
  • Refactor existing code (that's the next step)
bash
npx vitest run <test-file> --reporter=verbose
Checkpoint: All tests (new and existing) MUST pass.
编写最少的代码让所有测试通过。请勿:
  • 添加测试未覆盖的功能
  • 过早优化
  • 为未测试的场景添加错误处理
  • 重构现有代码(这是下一步的工作)
bash
npx vitest run <test-file> --reporter=verbose
检查点:所有测试(新测试和现有测试)必须通过。

6. Run the FULL Test Suite

6. 运行完整测试套件

Never skip this step:
bash
npx vitest run --reporter=verbose
If any tests outside your file fail:
  1. Determine if your change caused the regression
  2. If yes → fix it before proceeding
  3. If no (pre-existing failure) → note it but continue
绝对不要跳过这一步:
bash
npx vitest run --reporter=verbose
如果你的测试文件之外有测试失败:
  1. 判断是否是你的变更导致了回归
  2. 如果是 → 修复后再继续
  3. 如果不是(已有失败)→ 记录问题但继续后续步骤

7. Refactor (Optional)

7. 重构(可选)

If the implementation can be cleaner, refactor now while tests are green:
  • Extract helpers for repeated logic
  • Improve naming
  • Simplify conditionals
Re-run the full suite after any refactor.
如果实现代码可以更简洁,在测试全部通过的情况下进行重构:
  • 提取重复逻辑的辅助函数
  • 优化命名
  • 简化条件判断
每次重构后重新运行完整测试套件。

8. Quality Gates

8. 质量检查

Run lint and type checks on affected workspaces:
bash
npx eslint <changed-files>
npx tsc --noEmit
Fix any issues before committing.
对受影响的工作区运行代码检查和类型检查:
bash
npx eslint <changed-files>
npx tsc --noEmit
提交前修复所有问题。

9. Commit

9. 提交代码

Stage only the files you changed and commit:
type(scope): description

Co-Authored-By: Claude <noreply@anthropic.com>
只暂存你修改的文件并提交:
type(scope): description

Co-Authored-By: Claude <noreply@anthropic.com>

Arguments

参数说明

  • $ARGUMENTS
    : Optional description of what to implement via TDD
    • Example:
      /tdd add rate limiting to the search endpoint
    • If empty, ask the user what to implement
  • $ARGUMENTS
    :可选参数,用于指定要通过TDD实现的功能
    • 示例:
      /tdd add rate limiting to the search endpoint
    • 如果为空,请询问用户要实现的内容

Key Rules

核心规则

  1. Never write implementation before tests — this is the whole point of TDD
  2. Never skip step 3 — validate your mock pattern with ONE test before writing 20
  3. Never skip step 6 — the full suite must pass, not just your file
  4. Tests should fail for the RIGHT reason — a test that fails because of a missing import isn't a valid "red" test
  5. One logical change per cycle — don't batch multiple features into one TDD cycle
  6. When tests fail after refactor, question the TESTS first — they may have bad assumptions
  1. 绝对不要在编写测试前写实现代码——这是TDD的核心
  2. 绝对不要跳过步骤3——在编写20个测试前,先用一个测试验证你的模拟模式
  3. 绝对不要跳过步骤6——必须通过完整测试套件,而不仅仅是你的测试文件
  4. 测试必须因正确的原因失败——因缺少导入而失败的测试不是有效的“红阶段”测试
  5. 每个周期只做一个逻辑变更——不要在一个TDD周期中批量处理多个功能
  6. 重构后测试失败时,先检查测试用例——它们可能存在错误假设

Mock Patterns Reference

模拟模式参考

Forwarding Pattern (survives mockReset)

转发模式(支持mockReset)

typescript
const mockFn = vi.fn();
vi.mock("./dep", () => ({
  dep: (...args: unknown[]) => mockFn(...args),
}));
beforeEach(() => {
  mockFn.mockResolvedValue(defaultResult);
});
typescript
const mockFn = vi.fn();
vi.mock("./dep", () => ({
  dep: (...args: unknown[]) => mockFn(...args),
}));
beforeEach(() => {
  mockFn.mockResolvedValue(defaultResult);
});

Counter-Based Sequencing (multi-query operations)

基于计数器的顺序执行(多查询操作)

typescript
let callCount = 0;
mockExecute.mockImplementation(async () => {
  callCount++;
  if (callCount === 1) return insertResult;
  if (callCount === 2) return selectResult;
});
typescript
let callCount = 0;
mockExecute.mockImplementation(async () => {
  callCount++;
  if (callCount === 1) return insertResult;
  if (callCount === 2) return selectResult;
});

globalThis Registry (TDZ workaround)

globalThis 注册表(解决暂时性死区问题)

typescript
vi.mock("./dep", () => {
  if (!(globalThis as any).__mocks) (globalThis as any).__mocks = {};
  const m = { dep: vi.fn() };
  (globalThis as any).__mocks.dep = m;
  return { dep: (...a: unknown[]) => m.dep(...a) };
});
// In tests: const mocks = (globalThis as any).__mocks;
typescript
vi.mock("./dep", () => {
  if (!(globalThis as any).__mocks) (globalThis as any).__mocks = {};
  const m = { dep: vi.fn() };
  (globalThis as any).__mocks.dep = m;
  return { dep: (...a: unknown[]) => m.dep(...a) };
});
// In tests: const mocks = (globalThis as any).__mocks;