学习目标:掌握使用Claude Code构建完整测试金字塔的方法——从TDD红绿循环驱动单元测试,到集成测试验证模块交互,再到Playwright E2E测试覆盖关键用户路径。学完本教程,你能为一个真实项目建立80/15/5的测试分层体系,并通过CLAUDE.md规则和Hooks实现测试自动化门禁。
适用人群:已有Claude Code基础使用经验,想系统性提升代码质量保障能力的开发者。
前置条件:Node.js 18+、Claude Code CLI已安装、基础TypeScript能力。
传统测试金字塔在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测试运行慢但覆盖真实用户场景,只保留关键路径。
mkdir claude-test-pyramid && cd claude-test-pyramid
npm init -y
npm install -D vitest @vitest/coverage-v8 @playwright/test
npx playwright install chromium
创建 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
}
}
}
});
创建 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'
}
});
{
"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是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到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
先创建 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中输入:
所有单元测试都失败了。请实现 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,四个测试全部通过。
告诉Claude Code:
测试全部通过。请重构 validate.ts,使用策略模式来消除 if-else 链,
同时确保测试继续通过。
Claude Code会安全地重构代码,因为测试提供了安全网。运行测试验证重构没有引入回归。
继续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;
});
}
集成测试验证模块间的交互。创建 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。
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('高优先级');
}
});
});
在 .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修改文件后,自动触发单元测试。如果测试失败,你能立即看到错误信息。
在CLAUDE.md中添加更精确的TDD指令:
## TDD Enforcement
- 创建新功能时,先告诉用户"进入Red阶段",写测试
- 测试写完后运行测试确认失败,再进入Green阶段
- Green阶段只写最少代码让测试通过
- Refactor阶段优化代码,测试必须持续通过
- 每次完成一个Red-Green-Refactor循环后,报告覆盖率变化
## Bug Fix TDD
- 收到bug报告时,先写一个能复现bug的测试
- 确认测试失败,再修复bug
- 确认测试通过,检查没有引入新回归
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();
});
});
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意外改变了输出格式,快照测试会立即捕获。
在 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"
}
]
}
]
}
}
Claude Code的 -p 模式(pipe mode)非常适合在CI中运行完整的测试管道:
claude -p "运行完整测试套件(单元+集成+E2E),分析覆盖率报告。如果有测试失败或覆盖率低于80%,列出具体失败原因和改进建议。"
# .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
症状:测试依赖实现细节,重构时大量测试失败。
解决方案:在CLAUDE.md中增加规则:
## Test Quality Rules
- 测试只验证公开API的行为,不测试私有方法
- 不mock被测模块的内部函数
- 测试描述用"应该..."格式,不包含实现细节
- 如果一个测试需要超过3个mock,说明设计有问题
症状:Playwright测试套件需要10分钟以上。
解决方案:
## E2E Optimization
- E2E只覆盖3-5条关键用户路径
- 使用 parallel workers: `npx playwright test --workers=4`
- 利用 trace: 'on-first-retry' 减少截图开销
- 测试间不依赖执行顺序
症状:Claude Code倾向于先写实现再补测试。
解决方案:使用更明确的提示词:
请严格遵循TDD流程:
1. 只写测试,不写实现代码
2. 运行测试确认Red状态
3. 等我确认后再进入Green阶段
或者在CLAUDE.md中使用更强的约束语言:
## MANDATORY TDD
任何新功能或Bug修复,第一步必须是写测试。
违反此规则会导致代码质量下降,这是不可接受的。
如果我发现你先写了实现,我会要求你删除实现重新从测试开始。
症状:同一代码多次运行覆盖率结果不同。
解决方案:在 vitest.config.ts 中固定随机种子:
export default defineConfig({
test: {
poolOptions: {
forks: { singleFork: true } // 单线程避免竞态
}
}
});
将测试策略封装为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中定义的策略生成三层测试。
运行 npm run test:coverage 后,终端输出类似:
% Stmts % Branch % Funcs % Lines
--------- -------- ------- -------
85.71 80.00 100.00 85.71
告诉Claude Code分析覆盖率缺口:
分析覆盖率报告,找出未覆盖的分支和函数。
为每个缺口生成补充测试,目标是所有维度达到85%以上。
在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%+覆盖率的模块,包含完整三层测试 |
[!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 等场景全覆盖。
