mcpskills.net
技能MCP智能体提示词
mcpskills.net — A curated directory of AI agent Skills and MCP servers
TermsPrivacy
← 返回技能
Development

测试驱动开发

用测试来驱动开发。在实现任何逻辑、修复任何 Bug 或更改任何行为时使用。证明代码确实可用。

作者:Addy Osmani仓库 →来源 →

在编写让测试通过的代码之前,先编写一个会失败的测试。测试就是证明——"看起来没问题"不等于完成。

使用场景

  • 实现任何新逻辑或行为
  • 修复任何 Bug(即"证明它"模式)
  • 修改现有功能
  • 添加边界情况处理

不适用场景: 纯配置变更、文档更新或静态内容变更。

TDD(测试驱动开发)循环

    RED                GREEN              REFACTOR
 Write a test    Write minimal code    Clean up the
 that fails  ──→  to make it pass  ──→  implementation

步骤 1:RED —— 编写一个会失败的测试

// RED: This test fails because createTask doesn't exist yet
describe('TaskService', () => {
  it('creates a task with title and default status', async () => {
    const task = await taskService.createTask({ title: 'Buy groceries' });

    expect(task.id).toBeDefined();
    expect(task.title).toBe('Buy groceries');
    expect(task.status).toBe('pending');
    expect(task.createdAt).toBeInstanceOf(Date);
  });
});

步骤 2:GREEN —— 让它通过

// GREEN: Minimal implementation
export async function createTask(input: { title: string }): Promise<Task> {
  const task = {
    id: generateId(),
    title: input.title,
    status: 'pending' as const,
    createdAt: new Date(),
  };
  await db.tasks.insert(task);
  return task;
}

步骤 3:REFACTOR —— 清理

在测试通过的情况下,在不改变行为的前提下改进代码:

  • 抽取共享逻辑
  • 改进命名
  • 消除重复
  • 必要时进行优化

"证明它"模式(Bug 修复)

当收到 Bug 报告时,不要一开始就尝试去修复它。 先编写一个能复现该 Bug 的测试。

// Step 1: Write the reproduction test (it should FAIL)
it('sets completedAt when task is completed', async () => {
  const task = await taskService.createTask({ title: 'Test' });
  const completed = await taskService.completeTask(task.id);

  expect(completed.status).toBe('completed');
  expect(completed.completedAt).toBeInstanceOf(Date);  // This fails → bug confirmed
});

// Step 2: Fix the bug
export async function completeTask(id: string): Promise<Task> {
  return db.tasks.update(id, {
    status: 'completed',
    completedAt: new Date(),  // This was missing
  });
}

// Step 3: Test passes → bug fixed, regression guarded

测试金字塔

按照金字塔来分配测试投入:

          ╱╲
         ╱  ╲         E2E Tests (~5%)
        ╱    ╲        Full user flows, real browser
       ╱──────╲
      ╱        ╲      Integration Tests (~15%)
     ╱          ╲     Component interactions, API boundaries
    ╱────────────╲
   ╱              ╲   Unit Tests (~80%)
  ╱                ╲  Pure logic, isolated, milliseconds each
 ╱──────────────────╲

测试规模

| 规模 | 约束 | 速度 | 示例 | |------|------------|-------|---------| | 小型 | 单进程,无 I/O,无网络 | 毫秒级 | 纯函数测试 | | 中型 | 允许多进程,仅限 localhost | 秒级 | 使用测试数据库的 API 测试 | | 大型 | 允许多机器,外部服务 | 分钟级 | E2E 测试 |

编写优秀的测试

测试状态,而非交互

// Good: Tests what the function does (state-based)
it('returns tasks sorted by creation date', async () => {
  const tasks = await listTasks({ sortBy: 'createdAt', sortOrder: 'desc' });
  expect(tasks[0].createdAt.getTime())
    .toBeGreaterThan(tasks[1].createdAt.getTime());
});

// Bad: Tests how the function works internally
it('calls db.query with ORDER BY created_at DESC', async () => {
  await listTasks({ sortBy: 'createdAt', sortOrder: 'desc' });
  expect(db.query).toHaveBeenCalledWith(
    expect.stringContaining('ORDER BY created_at DESC')
  );
});

测试中 DAMP 优于 DRY

在测试中,DAMP(描述性且有意义的短语,Descriptive And Meaningful Phrases) 优于 DRY。每个测试都应该讲述一个完整的故事。

// DAMP: Each test is self-contained and readable
it('rejects tasks with empty titles', () => {
  const input = { title: '', assignee: 'user-1' };
  expect(() => createTask(input)).toThrow('Title is required');
});

it('trims whitespace from titles', () => {
  const input = { title: '  Buy groceries  ', assignee: 'user-1' };
  const task = createTask(input);
  expect(task.title).toBe('Buy groceries');
});

优先使用真实实现而非 Mock

Preference order (most to least preferred):
1. Real implementation  → Highest confidence
2. Fake                 → In-memory version
3. Stub                 → Returns canned data
4. Mock (interaction)   → Verifies method calls — use sparingly

使用 Arrange-Act-Assert 模式

it('marks overdue tasks when deadline has passed', () => {
  // Arrange: Set up the test scenario
  const task = createTask({
    title: 'Test',
    deadline: new Date('2025-01-01'),
  });

  // Act: Perform the action being tested
  const result = checkOverdue(task, new Date('2025-01-02'));

  // Assert: Verify the outcome
  expect(result.isOverdue).toBe(true);
});

应避免的测试反模式

| 反模式 | 问题 | 修复方法 | |---|---|---| | 测试实现细节 | 重构时测试会失败 | 测试输入与输出 | | 不稳定(Flaky)的测试 | 削弱对测试套件的信任 | 使用确定性断言 | | 测试框架代码 | 浪费时间 | 只测试你自己的代码 | | 滥用快照(Snapshot) | 没人审查的大型快照 | 谨慎使用并加以审查 | | 缺乏测试隔离 | 测试会一起失败 | 每个测试自行设置其状态 | | 对所有东西都 Mock | 测试通过但生产环境出问题 | 优先使用真实实现 |

验证

在完成任何实现之后:

  • [ ] 每个新行为都有对应的测试
  • [ ] 所有测试都通过:npm test
  • [ ] Bug 修复包含一个复现测试
  • [ ] 测试名称描述了所验证的行为
  • [ ] 没有任何测试被跳过或禁用
  • [ ] 覆盖率没有下降(如果有跟踪)