tdd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Test-Driven Development (TDD)

测试驱动开发(TDD)

Philosophy

理念

TDD is a development methodology where you write tests BEFORE implementation. This ensures:
  • Clear requirements - Tests define expected behavior upfront
  • Better design - Forces you to think about interfaces first
  • Confidence - Every feature has test coverage from day one
  • Refactoring safety - Tests catch regressions immediately
TDD是一种先编写测试再实现功能的开发方法论,它能确保:
  • 需求清晰:测试会预先定义预期行为
  • 更优设计:强制你优先考虑接口设计
  • 开发信心:每个功能从第一天起就有测试覆盖
  • 重构安全:测试能立即发现回归问题

When to Use TDD

何时使用TDD

SituationUse TDD?Reason
New utility functionYESPure functions are perfect for TDD
New Effect serviceYESDefine interface via tests first
Complex business logicYESTests clarify requirements
Bug fixYESWrite failing test that reproduces bug first
UI component stylingNoVisual changes don't benefit from TDD
Exploratory prototypingNoRequirements unclear, iterate first
TRPC endpoint (simple CRUD)OptionalAsk user preference

场景是否使用TDD理由
新增工具函数纯函数非常适合用TDD开发
新增Effect服务优先通过测试定义接口
复杂业务逻辑测试能明确需求
修复Bug先编写能复现Bug的失败测试
UI组件样式视觉改动无法从TDD中获益
探索性原型开发需求不清晰,优先快速迭代
TRPC端点(简单CRUD)可选询问用户偏好

Red-Green-Refactor Cycle

红-绿-重构周期

The TDD workflow follows three phases:
TDD工作流遵循三个阶段:

1. RED - Write Failing Test First

1. 红(RED) - 先编写失败的测试

typescript
import { describe, expect, it } from "vitest";
import { calculateDiscount } from "../calculate-discount";

describe("calculateDiscount", () => {
  it("applies 10% discount for orders over 100", () => {
    // This test will FAIL - function doesn't exist yet
    expect(calculateDiscount(150)).toBe(135);
  });
});
Run test to see it fail:
bash
bun run vitest run packages/common/src/__tests__/calculate-discount.test.ts
typescript
import { describe, expect, it } from "vitest";
import { calculateDiscount } from "../calculate-discount";

describe("calculateDiscount", () => {
  it("applies 10% discount for orders over 100", () => {
    // This test will FAIL - function doesn't exist yet
    expect(calculateDiscount(150)).toBe(135);
  });
});
运行测试确认它会失败:
bash
bun run vitest run packages/common/src/__tests__/calculate-discount.test.ts

2. GREEN - Minimal Implementation

2. 绿(GREEN) - 最小化实现

Write the minimum code to make the test pass:
typescript
// packages/common/src/calculate-discount.ts
export function calculateDiscount(amount: number): number {
  if (amount > 100) {
    return amount * 0.9;
  }
  return amount;
}
Run test to see it pass:
bash
bun run vitest run packages/common/src/__tests__/calculate-discount.test.ts
编写最少的代码让测试通过:
typescript
// packages/common/src/calculate-discount.ts
export function calculateDiscount(amount: number): number {
  if (amount > 100) {
    return amount * 0.9;
  }
  return amount;
}
运行测试确认通过:
bash
bun run vitest run packages/common/src/__tests__/calculate-discount.test.ts

3. REFACTOR - Improve Without Breaking

3. 重构(REFACTOR) - 不破坏功能的前提下优化代码

Improve code quality while keeping tests green:
typescript
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.1;

export function calculateDiscount(amount: number): number {
  if (amount <= DISCOUNT_THRESHOLD) {
    return amount;
  }
  return amount * (1 - DISCOUNT_RATE);
}
Run tests again to verify refactoring didn't break anything.
See
references/red-green-refactor.md
for detailed workflow examples.

在保持测试通过的前提下提升代码质量:
typescript
const DISCOUNT_THRESHOLD = 100;
const DISCOUNT_RATE = 0.1;

export function calculateDiscount(amount: number): number {
  if (amount <= DISCOUNT_THRESHOLD) {
    return amount;
  }
  return amount * (1 - DISCOUNT_RATE);
}
再次运行测试,确认重构没有破坏任何功能。
查看
references/red-green-refactor.md
获取详细的工作流示例。

Test Hierarchy (Prefer Simpler)

测试层级(优先选择更简单的方案)

  1. Unit tests (preferred) - Pure functions, Effect services with mock layers
  2. TRPC Integration (ask first) - Full TRPC stack with PGlite
  3. E2E (ask + justify) - Browser automation, slowest
SituationTest TypeAction
Pure function, parser, utilUnitWrite immediately
Effect service with dependenciesUnit with mock layersWrite immediately
TRPC procedure (DB logic)TRPC IntegrationAsk user first
User-facing flow, UI behaviorE2EAsk + warn about maintenance

  1. 单元测试(首选) - 纯函数、带模拟层的Effect服务测试
  2. TRPC集成测试(先询问) - 基于PGlite的完整TRPC栈测试
  3. E2E测试(需要询问+说明理由) - 浏览器自动化测试,执行速度最慢
场景测试类型操作建议
纯函数、解析器、工具函数单元测试直接编写
带依赖的Effect服务带模拟层的单元测试直接编写
TRPC过程(数据库逻辑)TRPC集成测试先询问用户
用户面向流程、UI行为E2E测试先询问+提示维护成本

Effect TDD Patterns

Effect TDD模式

Test-First Service Design

测试先行的服务设计

  1. Define interface via test - What should the service do?
  2. Create mock layer - Isolate dependencies
  3. Implement service - Make tests pass
  4. Refactor - Improve with confidence
typescript
import { describe, expect, it } from "@effect/vitest";
import { Effect, Layer } from "effect";

describe("PricingService", () => {
  // 1. Define what the service should do via tests
  it.effect("calculates base price without discount", () =>
    Effect.gen(function* () {
      const service = yield* PricingService;
      const result = yield* service.calculatePrice({
        itemId: "item-1",
        quantity: 2,
      });
      expect(result.total).toBe(200);
    }).pipe(Effect.provide(testLayer)),
  );

  it.effect("applies bulk discount for quantity > 10", () =>
    Effect.gen(function* () {
      const service = yield* PricingService;
      const result = yield* service.calculatePrice({
        itemId: "item-1",
        quantity: 15,
      });
      expect(result.total).toBe(1350); // 15% discount
    }).pipe(Effect.provide(testLayer)),
  );
});
  1. 通过测试定义接口 - 明确服务需要实现什么功能?
  2. 创建模拟层 - 隔离依赖
  3. 实现服务 - 让测试通过
  4. 重构 - 放心优化代码
typescript
import { describe, expect, it } from "@effect/vitest";
import { Effect, Layer } from "effect";

describe("PricingService", () => {
  // 1. Define what the service should do via tests
  it.effect("calculates base price without discount", () =>
    Effect.gen(function* () {
      const service = yield* PricingService;
      const result = yield* service.calculatePrice({
        itemId: "item-1",
        quantity: 2,
      });
      expect(result.total).toBe(200);
    }).pipe(Effect.provide(testLayer)),
  );

  it.effect("applies bulk discount for quantity > 10", () =>
    Effect.gen(function* () {
      const service = yield* PricingService;
      const result = yield* service.calculatePrice({
        itemId: "item-1",
        quantity: 15,
      });
      expect(result.total).toBe(1350); // 15% discount
    }).pipe(Effect.provide(testLayer)),
  );
});

Mock Layer Factory Pattern

模拟层工厂模式

typescript
// Create parameterized mock layers for different test scenarios
const createMockInventoryLayer = (inventory: Map<string, number>) =>
  Layer.succeed(InventoryService, {
    getStock: (itemId) => Effect.succeed(inventory.get(itemId) ?? 0),
    reserveStock: (itemId, qty) => Effect.succeed(void 0),
  });

// Use in tests
const testLayer = PricingService.layer.pipe(
  Layer.provide(createMockInventoryLayer(new Map([["item-1", 100]]))),
);
See
references/effect-tdd-patterns.md
for comprehensive Effect testing patterns.

typescript
// Create parameterized mock layers for different test scenarios
const createMockInventoryLayer = (inventory: Map<string, number>) =>
  Layer.succeed(InventoryService, {
    getStock: (itemId) => Effect.succeed(inventory.get(itemId) ?? 0),
    reserveStock: (itemId, qty) => Effect.succeed(void 0),
  });

// Use in tests
const testLayer = PricingService.layer.pipe(
  Layer.provide(createMockInventoryLayer(new Map([["item-1", 100]]))),
);
查看
references/effect-tdd-patterns.md
获取完整的Effect测试模式。

Test File Locations

测试文件位置

Code LocationTest Location
packages/X/src/file.ts
packages/X/src/__tests__/file.test.ts
apps/web-app/src/infrastructure/trpc/routers/X.ts
apps/web-app/src/__tests__/X.test.ts
apps/web-app/src/routes/**
apps/web-app/e2e/feature.e2e.ts

代码位置测试位置
packages/X/src/file.ts
packages/X/src/__tests__/file.test.ts
apps/web-app/src/infrastructure/trpc/routers/X.ts
apps/web-app/src/__tests__/X.test.ts
apps/web-app/src/routes/**
apps/web-app/e2e/feature.e2e.ts

Commands

命令

Unit & Integration Tests

单元&集成测试

bash
undefined
bash
undefined

Run all tests (unit + TRPC integration)

运行所有测试(单元 + TRPC集成)

bun run test
bun run test

Watch mode - re-run on file changes

监听模式 - 文件改动时自动重跑测试

bun run test:watch
bun run test:watch

Run with coverage report

运行测试并生成覆盖率报告

bun run test:coverage
bun run test:coverage

Run specific test file (FROM PROJECT ROOT, full path required)

运行指定测试文件(需要在项目根目录执行,传入完整路径)

bun run vitest run packages/common/src/tests/pagination.test.ts bun run vitest run apps/web-app/src/tests/formatters.test.ts
bun run vitest run packages/common/src/tests/pagination.test.ts bun run vitest run apps/web-app/src/tests/formatters.test.ts

Run tests matching pattern

运行匹配指定关键词的测试

bun run vitest run -t "calculateDiscount"
undefined
bun run vitest run -t "calculateDiscount"
undefined

E2E Tests

E2E测试

bash
undefined
bash
undefined

Install Playwright browsers (first time only)

安装Playwright浏览器(仅首次执行需要)

bun run test:e2e:install
bun run test:e2e:install

Run all E2E tests

运行所有E2E测试

bun run test:e2e
bun run test:e2e

Run E2E with interactive UI

带交互式UI运行E2E测试

bun run test:e2e:ui
undefined
bun run test:e2e:ui
undefined

WRONG Syntax (DO NOT USE)

错误语法(请勿使用)

bash
undefined
bash
undefined

These DO NOT work:

这些命令无法正常运行:

bun run test packages/common/src/tests/file.test.ts # script doesn't accept path cd packages/common && bun run vitest run src/tests/file.test.ts # wrong cwd

---
bun run test packages/common/src/tests/file.test.ts # 脚本不接收路径参数 cd packages/common && bun run vitest run src/tests/file.test.ts # 工作目录错误

---

TDD Anti-Patterns

TDD反模式

1. Writing Implementation First

1. 先写实现再写测试

typescript
// ❌ BAD - Implementation before test
export function formatPrice(amount: number): string {
  return `$${amount.toFixed(2)}`;
}

// Then writing test after - defeats TDD purpose
typescript
// ❌ 错误 - 先实现功能再写测试
export function formatPrice(amount: number): string {
  return `$${amount.toFixed(2)}`;
}

// 之后再补测试 - 违背了TDD的初衷

2. Skipping the RED Phase

2. 跳过红阶段

typescript
// ❌ BAD - Test passes immediately (you didn't verify it can fail)
it("returns true", () => {
  expect(true).toBe(true); // This always passes!
});
typescript
// ❌ 错误 - 测试一开始就会通过(你没有验证它可以失败)
it("returns true", () => {
  expect(true).toBe(true); // This always passes!
});

3. Too Many Tests at Once

3. 一次性编写太多测试

typescript
// ❌ BAD - Writing all tests before any implementation
describe("UserService", () => {
  it("creates user", () => {
    /* ... */
  });
  it("updates user", () => {
    /* ... */
  });
  it("deletes user", () => {
    /* ... */
  });
  it("lists users", () => {
    /* ... */
  });
  it("validates email", () => {
    /* ... */
  });
  // 10 more tests...
});
// Now you have 15 failing tests - overwhelming!
Correct approach: One test at a time. RED → GREEN → REFACTOR → next test.
typescript
// ❌ 错误 - 还没写任何实现就写完所有测试
describe("UserService", () => {
  it("creates user", () => {
    /* ... */
  });
  it("updates user", () => {
    /* ... */
  });
  it("deletes user", () => {
    /* ... */
  });
  it("lists users", () => {
    /* ... */
  });
  it("validates email", () => {
    /* ... */
  });
  // 还有10个测试...
});
// 现在你有15个失败的测试 - 压力太大了!
正确做法:一次只写一个测试,完成红→绿→重构循环后再写下一个测试。

4. Skipping Refactor Phase

4. 跳过重构阶段

typescript
// ❌ BAD - Test passes, move on without cleanup
export function calc(a: number, b: number, c: string): number {
  if (c === "add") return a + b;
  if (c === "sub") return a - b;
  if (c === "mul") return a * b;
  return 0;
}

// ✅ GOOD - Refactor to cleaner design
type Operation = "add" | "subtract" | "multiply";

const operations: Record<Operation, (a: number, b: number) => number> = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
};

export function calculate(a: number, b: number, op: Operation): number {
  return operations[op](a, b);
}

typescript
// ❌ 错误 - 测试通过就不管了,不做代码清理
export function calc(a: number, b: number, c: string): number {
  if (c === "add") return a + b;
  if (c === "sub") return a - b;
  if (c === "mul") return a * b;
  return 0;
}

// ✅ 正确 - 重构为更清晰的设计
type Operation = "add" | "subtract" | "multiply";

const operations: Record<Operation, (a: number, b: number) => number> = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
};

export function calculate(a: number, b: number, op: Operation): number {
  return operations[op](a, b);
}

Resources

资源

references/

references/目录下文件

  • red-green-refactor.md
    - Detailed TDD cycle workflow with examples
  • effect-tdd-patterns.md
    - Effect service testing, mock layers, error cases
  • test-first-examples.md
    - Step-by-step TDD examples for this codebase
  • red-green-refactor.md
    - 带示例的详细TDD周期工作流
  • effect-tdd-patterns.md
    - Effect服务测试、模拟层、错误场景处理
  • test-first-examples.md
    - 本代码库的分步TDD示例

Related Skills

相关技能

  • testing-patterns
    - Test syntax, TRPC integration tests, E2E patterns
  • effect-ts
    - Effect service design, layers, error handling
  • testing-patterns
    - 测试语法、TRPC集成测试、E2E模式
  • effect-ts
    - Effect服务设计、层、错误处理