首页 › tutorials › 测试金字塔实战:TDD红绿到E2E全链路

Claude Code测试金字塔实战:TDD红绿循环到E2E全链路

学习目标:掌握使用Claude Code构建完整测试金字塔的方法——从TDD红绿循环驱动单元测试,到集成测试验证模块交互,再到Playwright E2E测试覆盖关键用户路径。学完本教程,你能为一个真实项目建立80/15/5的测试分层体系,并通过CLAUDE.md规则和Hooks实现测试自动化门禁。

适用人群:已有Claude Code基础使用经验,想系统性提升代码质量保障能力的开发者。

前置条件:Node.js 18+、Claude Code CLI已安装、基础TypeScript能力。


一、测试金字塔:AI辅助开发的正确分层

传统测试金字塔在AI辅助开发下有了新的内涵。Claude Code生成的代码速度快但需要更严格的质量关卡——测试不再是"写完代码再补",而是驱动开发流程的核心引擎。

graph TB
    subgraph 测试金字塔
        E2E["E2E测试 5%<br/>Playwright<br/>关键用户路径"]
        INT["集成测试 15%<br/>Vitest<br/>模块交互验证"]
        UNIT["单元测试 80%<br/>Vitest<br/>纯函数/逻辑"]
    end
    UNIT --> INT --> E2E

    subgraph Claude Code自动化
        CL["CLAUDE.md测试规则"]
        HK["Hooks门禁"]
        TD["TDD Skill"]
    end

    CL --> UNIT
    HK --> INT
    TD --> E2E

    style UNIT fill:#4CAF50,color:#fff
    style INT fill:#FF9800,color:#fff
    style E2E fill:#F44336,color:#fff

为什么是80/15/5? 单元测试运行毫秒级,Claude Code可以在数秒内生成和验证数百个单元测试用例;集成测试验证模块间接口契约,数量适中;E2E测试运行慢但覆盖真实用户场景,只保留关键路径。


二、环境搭建:一个命令搞定测试基础设施

2.1 项目初始化

mkdir claude-test-pyramid && cd claude-test-pyramid
npm init -y
npm install -D vitest @vitest/coverage-v8 @playwright/test
npx playwright install chromium

2.2 Vitest配置

创建 vitest.config.ts:

import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['src/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      exclude: ['**/*.test.ts', 'src/types/**'],
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80
      }
    }
  }
});

2.3 Playwright配置

创建 playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  timeout: 30000,
  retries: 1,
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure'
  }
});

2.4 在package.json中添加脚本

{
  "scripts": {
    "test:unit": "vitest run",
    "test:unit:watch": "vitest",
    "test:integration": "vitest run --config vitest.integration.config.ts",
    "test:e2e": "playwright test",
    "test:all": "npm run test:unit && npm run test:integration && npm run test:e2e",
    "test:coverage": "vitest run --coverage"
  }
}

三、CLAUDE.md测试规则:让AI理解你的测试策略

CLAUDE.md是Claude Code测试工作流的核心配置文件。它告诉Claude如何生成测试、遵循什么规则、使用什么模式。

在项目根目录创建 CLAUDE.md:

# Testing Rules

## Testing Framework
- 单元测试和集成测试使用 Vitest
- E2E测试使用 Playwright
- 测试文件与源文件同目录,命名为 `*.test.ts`

## Testing Pyramid
- 80% 单元测试:纯函数、工具类、业务逻辑
- 15% 集成测试:模块交互、数据库操作、API调用
- 5% E2E测试:关键用户路径(登录、支付、数据提交)

## TDD Workflow
- 新功能必须先写测试再写实现(Red-Green-Refactor)
- Bug修复必须先写复现测试

## Test Quality
- 每个测试遵循 AAA 模式:Arrange-Act-Assert
- 测试描述使用中文,清晰表达意图
- 禁止测试实现细节,只测行为
- Mock只用于外部依赖,不mock内部模块

## Coverage
- 目标覆盖率:80% 行、80% 分支、80% 函数
- 覆盖率不达标不允许提交

## Commands
- 运行单元测试:npm run test:unit
- 运行E2E测试:npm run test:e2e
- 查看覆盖率:npm run test:coverage

关键点:CLAUDE.md中明确声明测试框架、分层策略和TDD工作流,Claude Code在后续所有对话中都会遵守这些规则。


四、实战案例:任务管理系统(TDD全流程)

我们以一个任务管理系统为例,完整演示从TDD到E2E的全链路测试。项目包含三个核心模块:

graph LR
    subgraph 源码
        VS["validate.ts<br/>输入校验"]
        TF["task-filter.ts<br/>过滤引擎"]
        TS["task-service.ts<br/>业务逻辑"]
    end

    subgraph 测试层
        UT["单元测试<br/>validate.test.ts<br/>task-filter.test.ts"]
        IT["集成测试<br/>task-service.test.ts"]
        ET["E2E测试<br/>task-crud.spec.ts"]
    end

    VS --> UT
    TF --> UT
    TS --> IT
    IT --> ET

    style UT fill:#4CAF50,color:#fff
    style IT fill:#FF9800,color:#fff
    style ET fill:#F44336,color:#fff

4.1 第一层:TDD驱动单元测试

步骤一:Red——写失败测试

先创建 src/validate.ts 的测试文件 src/validate.test.ts:

import { describe, it, expect } from 'vitest';
import { validateTask, ValidationError } from './validate';

describe('validateTask', () => {
  it('应该拒绝空标题', () => {
    expect(() => validateTask({ title: '' }))
      .toThrow(ValidationError);
  });

  it('应该拒绝超过200字的标题', () => {
    const longTitle = 'a'.repeat(201);
    expect(() => validateTask({ title: longTitle }))
      .toThrow('标题不能超过200个字符');
  });

  it('应该拒绝无效的优先级值', () => {
    expect(() => validateTask({ title: '测试', priority: 'invalid' as any }))
      .toThrow('优先级必须是 low/medium/high');
  });

  it('应该通过合法的任务数据', () => {
    const result = validateTask({
      title: '完成报告',
      priority: 'high',
      dueDate: '2026-06-01'
    });
    expect(result.valid).toBe(true);
    expect(result.data.title).toBe('完成报告');
  });
});

此时运行测试必定失败——因为 validate.ts 还不存在。

步骤二:告诉Claude Code进入Green阶段

在Claude Code中输入:

所有单元测试都失败了。请实现 src/validate.ts 让测试通过。
遵循CLAUDE.md中的测试规则,不要过度设计。

Claude Code会生成:

// src/validate.ts
export class ValidationError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'ValidationError';
  }
}

interface TaskInput {
  title: string;
  priority?: string;
  dueDate?: string;
}

interface ValidationResult {
  valid: boolean;
  data: TaskInput;
}

export function validateTask(input: TaskInput): ValidationResult {
  if (!input.title || input.title.trim() === '') {
    throw new ValidationError('标题不能为空');
  }
  if (input.title.length > 200) {
    throw new ValidationError('标题不能超过200个字符');
  }
  if (input.priority && !['low', 'medium', 'high'].includes(input.priority)) {
    throw new ValidationError('优先级必须是 low/medium/high');
  }
  return { valid: true, data: input };
}

运行 npm run test:unit,四个测试全部通过。

步骤三:Refactor——优化实现

告诉Claude Code:

测试全部通过。请重构 validate.ts,使用策略模式来消除 if-else 链,
同时确保测试继续通过。

Claude Code会安全地重构代码,因为测试提供了安全网。运行测试验证重构没有引入回归。

4.2 第二个单元测试模块:过滤引擎

继续TDD流程,先写 src/task-filter.test.ts:

import { describe, it, expect } from 'vitest';
import { filterTasks, Task } from './task-filter';

const sampleTasks: Task[] = [
  { id: '1', title: '写文档', priority: 'high', status: 'todo', tags: ['work'] },
  { id: '2', title: '买菜', priority: 'low', status: 'done', tags: ['life'] },
  { id: '3', title: '修bug', priority: 'high', status: 'in-progress', tags: ['work'] },
  { id: '4', title: '跑步', priority: 'medium', status: 'todo', tags: ['health'] },
];

describe('filterTasks', () => {
  it('按优先级过滤', () => {
    const result = filterTasks(sampleTasks, { priority: 'high' });
    expect(result).toHaveLength(2);
    expect(result.every(t => t.priority === 'high')).toBe(true);
  });

  it('按状态过滤', () => {
    const result = filterTasks(sampleTasks, { status: 'todo' });
    expect(result).toHaveLength(2);
  });

  it('按标签过滤', () => {
    const result = filterTasks(sampleTasks, { tag: 'work' });
    expect(result).toHaveLength(2);
  });

  it('组合过滤:高优先级 + 未完成', () => {
    const result = filterTasks(sampleTasks, { priority: 'high', status: 'todo' });
    expect(result).toHaveLength(1);
    expect(result[0].title).toBe('写文档');
  });

  it('无匹配结果返回空数组', () => {
    const result = filterTasks(sampleTasks, { priority: 'low', status: 'todo' });
    expect(result).toEqual([]);
  });

  it('空过滤条件返回全部', () => {
    const result = filterTasks(sampleTasks, {});
    expect(result).toHaveLength(4);
  });
});

让Claude Code生成实现:

// src/task-filter.ts
export interface Task {
  id: string;
  title: string;
  priority: 'low' | 'medium' | 'high';
  status: 'todo' | 'in-progress' | 'done';
  tags: string[];
}

interface FilterCriteria {
  priority?: string;
  status?: string;
  tag?: string;
}

export function filterTasks(tasks: Task[], criteria: FilterCriteria): Task[] {
  return tasks.filter(task => {
    if (criteria.priority && task.priority !== criteria.priority) return false;
    if (criteria.status && task.status !== criteria.status) return false;
    if (criteria.tag && !task.tags.includes(criteria.tag)) return false;
    return true;
  });
}

4.3 第二层:集成测试

集成测试验证模块间的交互。创建 tests/integration/task-service.test.ts:

import { describe, it, expect, beforeEach } from 'vitest';
import { TaskService } from '../../src/task-service';
import { validateTask } from '../../src/validate';

describe('TaskService 集成测试', () => {
  let service: TaskService;

  beforeEach(() => {
    service = new TaskService();
  });

  it('创建任务应该通过校验并持久化', () => {
    const task = service.create({
      title: '集成测试任务',
      priority: 'high'
    });

    expect(task.id).toBeDefined();
    expect(task.status).toBe('todo');

    const found = service.findById(task.id);
    expect(found).toEqual(task);
  });

  it('创建非法任务应该抛出ValidationError', () => {
    expect(() => service.create({ title: '' }))
      .toThrow();
  });

  it('任务状态流转:todo → in-progress → done', () => {
    const task = service.create({ title: '状态流转测试' });
    expect(task.status).toBe('todo');

    const started = service.start(task.id);
    expect(started.status).toBe('in-progress');

    const completed = service.complete(task.id);
    expect(completed.status).toBe('done');
  });

  it('不能重复启动已进行中的任务', () => {
    const task = service.create({ title: '重复操作测试' });
    service.start(task.id);
    expect(() => service.start(task.id)).toThrow('任务已在进行中');
  });

  it('不能完成未启动的任务', () => {
    const task = service.create({ title: '跳步测试' });
    expect(() => service.complete(task.id)).toThrow('任务必须先启动');
  });

  it('过滤功能与任务管理联动', () => {
    service.create({ title: '工作A', priority: 'high' });
    service.create({ title: '生活B', priority: 'low' });
    service.create({ title: '工作C', priority: 'high' });

    const highPriority = service.filter({ priority: 'high' });
    expect(highPriority).toHaveLength(2);
  });
});

告诉Claude Code:

请实现 src/task-service.ts,整合 validate.ts 和 task-filter.ts,
让集成测试全部通过。不要mock内部模块。

Claude Code会生成完整的 TaskService 实现,内部调用 validateTask 和 filterTasks。

4.4 第三层:E2E测试

E2E测试覆盖关键用户路径。假设任务管理器有一个简单的Web界面:

// e2e/task-crud.spec.ts
import { test, expect } from '@playwright/test';

test.describe('任务管理 CRUD', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/');
  });

  test('创建新任务并在列表中显示', async ({ page }) => {
    await page.fill('[data-testid="task-title-input"]', 'E2E测试任务');
    await page.selectOption('[data-testid="task-priority-select"]', 'high');
    await page.click('[data-testid="task-submit-btn"]');

    await expect(page.locator('.task-item').last())
      .toContainText('E2E测试任务');
    await expect(page.locator('.task-item').last())
      .toContainText('高优先级');
  });

  test('完成任务后状态变更', async ({ page }) => {
    // 假设已有一个任务
    await page.click('.task-item:first-child .start-btn');
    await page.click('.task-item:first-child .complete-btn');

    await expect(page.locator('.task-item:first-child'))
      .toHaveClass(/completed/);
  });

  test('空标题提交显示校验错误', async ({ page }) => {
    await page.fill('[data-testid="task-title-input"]', '');
    await page.click('[data-testid="task-submit-btn"]');

    await expect(page.locator('.error-message'))
      .toContainText('标题不能为空');
  });

  test('按优先级过滤任务', async ({ page }) => {
    await page.selectOption('[data-testid="filter-priority"]', 'high');

    const items = page.locator('.task-item');
    const count = await items.count();
    for (let i = 0; i < count; i++) {
      await expect(items.nth(i)).toContainText('高优先级');
    }
  });
});

五、TDD红绿循环自动化:Hooks门禁

5.1 配置Pre-commit Hook

在 .claude/settings.json 中配置Hooks,强制每次代码变更后运行相关测试:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run test:unit -- --reporter=verbose 2>&1 | tail -20"
          }
        ]
      }
    ]
  }
}

这样每次Claude Code修改文件后,自动触发单元测试。如果测试失败,你能立即看到错误信息。

5.2 TDD工作流的CLAUDE.md增强规则

在CLAUDE.md中添加更精确的TDD指令:

## TDD Enforcement
- 创建新功能时,先告诉用户"进入Red阶段",写测试
- 测试写完后运行测试确认失败,再进入Green阶段
- Green阶段只写最少代码让测试通过
- Refactor阶段优化代码,测试必须持续通过
- 每次完成一个Red-Green-Refactor循环后,报告覆盖率变化

## Bug Fix TDD
- 收到bug报告时,先写一个能复现bug的测试
- 确认测试失败,再修复bug
- 确认测试通过,检查没有引入新回归

六、测试生成策略:让Claude Code高效产出高质量测试

6.1 边界值与异常路径

Claude Code的测试生成能力强大,但需要明确引导。告诉它:

为 src/task-filter.ts 生成边界值测试和异常路径测试。
考虑以下场景:
- 空任务列表
- undefined/null字段
- 超长字符串输入
- 并发修改
- 不存在的ID查询

Claude Code会生成类似:

describe('filterTasks 边界值', () => {
  it('空数组应返回空数组', () => {
    expect(filterTasks([], { priority: 'high' })).toEqual([]);
  });

  it('超长标签不应崩溃', () => {
    const task: Task = {
      id: '1', title: 'test', priority: 'low',
      status: 'todo', tags: ['x'.repeat(10000)]
    };
    expect(() => filterTasks([task], { tag: 'x' })).not.toThrow();
  });
});

6.2 快照测试:防止意外变更

import { describe, it, expect } from 'vitest';
import { formatTaskSummary } from './formatters';

describe('formatTaskSummary 快照测试', () => {
  it('任务摘要格式稳定', () => {
    const result = formatTaskSummary({
      id: '1',
      title: '完成季度报告',
      priority: 'high',
      status: 'in-progress',
      tags: ['work', 'urgent']
    });
    expect(result).toMatchSnapshot();
  });
});

快照测试非常适合Claude Code场景——如果AI意外改变了输出格式,快照测试会立即捕获。


七、覆盖率门禁与CI集成

7.1 本地覆盖率门禁

在 vitest.config.ts 中已配置了覆盖率阈值。当覆盖率低于80%时,Vitest会以非零退出码退出。配合Hooks使用:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run test:coverage 2>&1 | grep -E '(FAIL|Coverage|threshold)' | tail -10"
          }
        ]
      }
    ]
  }
}

7.2 使用 -p 模式运行测试管道

Claude Code的 -p 模式(pipe mode)非常适合在CI中运行完整的测试管道:

claude -p "运行完整测试套件(单元+集成+E2E),分析覆盖率报告。如果有测试失败或覆盖率低于80%,列出具体失败原因和改进建议。"

7.3 GitHub Actions集成

# .github/workflows/test.yml
name: Test Pyramid
on: [push, pull_request]
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run test:coverage
      - name: 覆盖率报告
        uses: davelosert/vitest-coverage-report-action@v2

  e2e:
    runs-on: ubuntu-latest
    needs: unit
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npm run test:e2e

八、完整工作流程图

flowchart TD
    A[收到需求/Bug报告] --> B{类型判断}
    B -->|新功能| C[Red: 写失败测试]
    B -->|Bug修复| D[写复现测试]

    C --> E[运行测试 确认失败]
    D --> E

    E --> F[Green: Claude Code实现最少代码]
    F --> G[运行测试 确认通过]

    G --> H[Refactor: 优化代码]
    H --> I[运行测试 确认无回归]

    I --> J{覆盖率≥80%?}
    J -->|是| K[提交代码]
    J -->|否| L[Claude Code补充测试]
    L --> I

    K --> M[CI运行完整测试金字塔]
    M --> N{全部通过?}
    N -->|是| O[合并代码]
    N -->|否| P[分析失败原因]
    P --> A

    style C fill:#F44336,color:#fff
    style F fill:#4CAF50,color:#fff
    style H fill:#2196F3,color:#fff
    style K fill:#9C27B0,color:#fff
    style O fill:#4CAF50,color:#fff

九、常见问题与解决方案

问题1:Claude Code生成的测试太脆弱

症状:测试依赖实现细节,重构时大量测试失败。

解决方案:在CLAUDE.md中增加规则:

## Test Quality Rules
- 测试只验证公开API的行为,不测试私有方法
- 不mock被测模块的内部函数
- 测试描述用"应该..."格式,不包含实现细节
- 如果一个测试需要超过3个mock,说明设计有问题

问题2:E2E测试运行太慢

症状:Playwright测试套件需要10分钟以上。

解决方案:

## E2E Optimization
- E2E只覆盖3-5条关键用户路径
- 使用 parallel workers: `npx playwright test --workers=4`
- 利用 trace: 'on-first-retry' 减少截图开销
- 测试间不依赖执行顺序

问题3:Claude Code跳过TDD直接写实现

症状:Claude Code倾向于先写实现再补测试。

解决方案:使用更明确的提示词:

请严格遵循TDD流程:
1. 只写测试,不写实现代码
2. 运行测试确认Red状态
3. 等我确认后再进入Green阶段

或者在CLAUDE.md中使用更强的约束语言:

## MANDATORY TDD
任何新功能或Bug修复,第一步必须是写测试。
违反此规则会导致代码质量下降,这是不可接受的。
如果我发现你先写了实现,我会要求你删除实现重新从测试开始。

问题4:覆盖率数据不稳定

症状:同一代码多次运行覆盖率结果不同。

解决方案:在 vitest.config.ts 中固定随机种子:

export default defineConfig({
  test: {
    poolOptions: {
      forks: { singleFork: true }  // 单线程避免竞态
    }
  }
});

十、进阶:测试策略Skill

将测试策略封装为Claude Code Skill,让测试规范自动生效。创建 .claude/skills/test-pyramid/SKILL.md:

---
name: test-pyramid
description: 测试金字塔策略执行器,自动为代码生成三层测试
---

## 触发条件
- 用户要求测试某个模块
- 代码变更后需要补充测试

## 执行步骤
1. 分析目标模块的公开API
2. 生成单元测试(80%):覆盖所有公开函数的happy path和edge case
3. 生成集成测试(15%:覆盖模块间交互
4. 生成E2E测试大纲(5%:列出关键用户场景
5. 运行所有测试,报告结果和覆盖率

## 测试模板

### 单元测试
- 每个函数至少3个测试:happy path、边界值、异常
- 使用describe/it组织,中文描述
- 遵循AAA模式

### 集成测试
- 使用真实的依赖,不mock内部模块
- 测试完整的状态流转
- 验证数据持久化

### E2E测试
- 只覆盖关键路径
- 使用data-testid选择器
- 测试间独立可并行

使用时只需在Claude Code中输入:

/test-pyramid src/task-service.ts

Claude Code会自动按照Skill中定义的策略生成三层测试。


十一、测试报告与度量

11.1 覆盖率报告解读

运行 npm run test:coverage 后,终端输出类似:

 % Stmts   % Branch  % Funcs   % Lines
---------  --------  -------  -------
  85.71    80.00     100.00    85.71

告诉Claude Code分析覆盖率缺口:

分析覆盖率报告,找出未覆盖的分支和函数。
为每个缺口生成补充测试,目标是所有维度达到85%以上。

11.2 测试趋势追踪

在CI中收集测试数据,生成趋势报告:

claude -p "分析最近的测试运行结果。对比本次和上次覆盖率变化,列出:1)新增的测试 2)覆盖率变化 3)需要关注的模块。输出为markdown表格。"

十二、训练营适用性评估

维度 评估
适合训练营类型 全栈开发训练营、DevOps训练营、代码质量训练营
预计教学时长 4-6小时(含动手实践)
难度等级 中级(需要基础TypeScript和测试概念)
实验环境要求 Node.js 18+、Claude Code CLI、Chrome浏览器
最佳学习路径 先做单元测试TDD(1.5h)→ 集成测试(1h)→ E2E测试(1.5h)→ Hooks和CI(1h)
考核方式 提交一个有80%+覆盖率的模块,包含完整三层测试

延伸阅读


参考来源

  1. Claude Code Testing: Automating Unit Tests, E2E & TDD (2026) — Claude Code测试自动化完整指南,覆盖TDD和E2E流程
  2. Forcing Claude Code to TDD: An Agentic Red-Green-Refactor Loop — 使用Skill和SubAgent强制执行TDD红绿循环的实战方案
  3. The Complete Claude Code Testing Workflow Guide: From TDD to CI/CD — 从TDD到CI/CD的系统性测试工作流指南
  4. Claude Code with Vitest: Mocks, Coverage, Modern Testing — Vitest与Claude Code集成的配置和Mock策略
  5. Automated TDD with Claude Code: Testing Strategy for AI-Assisted Development — 80/15/5测试金字塔分布的实战策略和自动化方案

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入