tdd
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseTDD 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.tssrc/foo.test.ts - Test directory: →
src/routes/bar.tssrc/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 block.
describe()按照项目规范定位或创建测试文件:
- 同目录测试:→
src/foo.tssrc/foo.test.ts - 测试目录:→
src/routes/bar.tssrc/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:
- Check test runner config for /
mockReset/mockClearsettings:restoreMocks
bash
grep -rn 'mockReset\|mockClear\|restoreMocks' vitest.config.* jest.config.*If is set, mocks are reset between tests — you MUST reconfigure return values in , not at module scope.
mockReset: truebeforeEach- 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 });
});
});- Run it and confirm it passes:
bash
npx vitest run <test-file> --reporter=verbose- Only then proceed to write the full test suite following that proven mock pattern.
Why this matters: moves mock declarations above hoisting, avoiding temporal dead zone issues. Validating one mock round-trip before writing 20 tests saves significant debugging time.
vi.hoisted()vi.mock()在编写任何测试前,先验证你的模拟对象能否正常工作:
- 检查测试运行器配置中的/
mockReset/mockClear设置:restoreMocks
bash
grep -rn 'mockReset\|mockClear\|restoreMocks' vitest.config.* jest.config.*如果设置了,测试之间会重置模拟对象——你必须在中重新配置返回值,而不是在模块作用域中配置。
mockReset: truebeforeEach- 编写一个最简测试用例,导入待测试模块并验证模拟对象能正确解析:
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 });
});
});- 运行测试并确认通过:
bash
npx vitest run <test-file> --reporter=verbose- 只有通过后,再按照已验证的模拟模式编写完整测试套件。
为什么这很重要:会将模拟声明移到提升之前,避免暂时性死区问题。在编写20个测试前先验证一次模拟往返,能节省大量调试时间。
vi.hoisted()vi.mock()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=verboseCheckpoint: 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=verboseCheckpoint: 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=verboseIf any tests outside your file fail:
- Determine if your change caused the regression
- If yes → fix it before proceeding
- If no (pre-existing failure) → note it but continue
绝对不要跳过这一步:
bash
npx vitest run --reporter=verbose如果你的测试文件之外有测试失败:
- 判断是否是你的变更导致了回归
- 如果是 → 修复后再继续
- 如果不是(已有失败)→ 记录问题但继续后续步骤
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 --noEmitFix 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
参数说明
- : Optional description of what to implement via TDD
$ARGUMENTS- Example:
/tdd add rate limiting to the search endpoint - If empty, ask the user what to implement
- Example:
- :可选参数,用于指定要通过TDD实现的功能
$ARGUMENTS- 示例:
/tdd add rate limiting to the search endpoint - 如果为空,请询问用户要实现的内容
- 示例:
Key Rules
核心规则
- Never write implementation before tests — this is the whole point of TDD
- Never skip step 3 — validate your mock pattern with ONE test before writing 20
- Never skip step 6 — the full suite must pass, not just your file
- Tests should fail for the RIGHT reason — a test that fails because of a missing import isn't a valid "red" test
- One logical change per cycle — don't batch multiple features into one TDD cycle
- When tests fail after refactor, question the TESTS first — they may have bad assumptions
- 绝对不要在编写测试前写实现代码——这是TDD的核心
- 绝对不要跳过步骤3——在编写20个测试前,先用一个测试验证你的模拟模式
- 绝对不要跳过步骤6——必须通过完整测试套件,而不仅仅是你的测试文件
- 测试必须因正确的原因失败——因缺少导入而失败的测试不是有效的“红阶段”测试
- 每个周期只做一个逻辑变更——不要在一个TDD周期中批量处理多个功能
- 重构后测试失败时,先检查测试用例——它们可能存在错误假设
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;