name: testing-fundamentals description: 指导单元测试、集成测试和端到端测试的策略。用于当新手问“如何测试”、“写测试”、“应该测试什么”、“测试覆盖率”、“模拟”,或使用Vitest、Jest、Playwright时。提供测试金字塔和AAA模式。
测试基础回顾
“如果你不能测试它,你就没有理解它。测试是理解的证明。”
何时应用
在以下情况激活此技能:
- 审查测试文件(*.test.ts, *.spec.ts, *.test.js)
- 新手询问测试策略
- 完成一个没有测试的功能
- 讨论覆盖率或测试质量
测试金字塔
▲
╱ ╲ 端到端测试 (10%)
╱ ╲ Playwright - 完整用户流程
╱─────╲
╱ ╲ 集成测试 (20%)
╱ ╲ Vitest + RTL - 组件交互
╱───────────╲
╱ ╲ 单元测试 (70%)
╱ ╲ Vitest - 函数、工具、逻辑
─────────────────
- 单元测试 (70%): 快速、隔离、测试一件事
- 集成测试 (20%): 组件一起工作
- 端到端测试 (10%): 仅关键用户旅程
堆栈特定框架指南
| 堆栈 | 单元/集成 | 端到端 |
|---|---|---|
| Vite + React | Vitest + React Testing Library | Playwright |
| Create React App | Jest + RTL | Playwright |
| Next.js | Vitest 或 Jest + RTL | Playwright |
| Node.js | Vitest (原生ESM) | - |
| Python | pytest | - |
| Go | go test | - |
为什么在Vite中使用Vitest?
- 在监视模式下比Jest快10-20倍
- 原生ESM支持
- 与Vite相同配置(vite.config.ts)
- 兼容Jest API
测试什么(三个问题)
- 快乐路径: 当一切正常时,它是否工作?
- 边界情况: 空值、null、最大值时会发生什么?
- 错误状态: 它是否优雅地失败?
好的测试检查:
- [ ] 组件渲染不崩溃
- [ ] 用户交互工作(点击、输入、提交)
- [ ] 错误状态正确显示
- [ ] 加载状态出现和消失
- [ ] 数据正确流经组件
坏的测试检查:
- [ ] 实现细节(内部状态、方法调用)
- [ ] 样式或CSS类
- [ ] 第三方库内部
- [ ] 大组件的快照测试(脆弱)
常见错误(反模式)
1. 测试实现,而非行为
// ❌ 错误:测试内部状态
expect(component.state.isLoading).toBe(true);
// ✅ 正确:测试用户看到的
expect(screen.getByText('加载中...')).toBeInTheDocument();
2. 过度模拟
// ❌ 错误:模拟一切
jest.mock('./utils');
jest.mock('./api');
jest.mock('./hooks');
// 此时你甚至测试什么?
// ✅ 正确:仅模拟外部边界
vi.mock('./api'); // 模拟API,测试其余部分
3. 测试第三方库
// ❌ 错误:测试React Query是否工作
expect(useQuery).toHaveBeenCalledWith('users');
// ✅ 正确:测试你的代码行为
await waitFor(() => {
expect(screen.getByText('用户名')).toBeInTheDocument();
});
4. 脆弱的选择器
// ❌ 错误:如果更改CSS会中断
screen.getByClassName('btn-primary-large-blue');
// ✅ 正确:语义化和稳定
screen.getByRole('button', { name: '提交' });
5. 测试一切
// ❌ 错误:100%覆盖率目标
// 导致仅为了覆盖率而存在的测试
// ✅ 正确:战略性覆盖率
// 测试关键路径、边界情况、复杂逻辑
苏格拉底式问题
问这些而不是给出答案:
- 策略: “最需要测试的关键用户流程是什么?”
- 覆盖率: “如果这个测试通过但功能坏了,你怎么知道?”
- 边界情况: “什么输入可能破坏这个?空?null?10,000个项目?”
- 隔离: “你是在测试你的代码还是库的代码?”
- 价值: “这个测试会捕获真正的错误吗?”
测试结构(AAA模式)
describe('登录表单', () => {
it('当密码太短时显示错误', async () => {
// 安排
render(<LoginForm />);
// 行动
await userEvent.type(screen.getByLabelText('密码'), '123');
await userEvent.click(screen.getByRole('button', { name: '登录' }));
// 断言
expect(screen.getByText('密码必须至少8个字符')).toBeInTheDocument();
});
});
需要指出的红旗
| 红旗 | 问题 |
|---|---|
| 功能没有测试 | “什么测试证明这个工作?” |
| 只测试了快乐路径 | “如果API失败呢?如果输入为空呢?” |
| 模拟一切 | “你实际上在这里测试什么?” |
| 测试实现 | “如果你重构但行为保持不变,这个测试会中断吗?” |
| 使用getByClassName | “有更语义化的方式选择这个元素吗?” |
| 大快照测试 | “当它改变时,你真的会审查这个差异吗?” |
MCP 使用
Context7 - 框架文档
获取:Vitest 文档
获取:React Testing Library 查询
获取:Playwright 最佳实践
Octocode - 真实示例
搜索:流行仓库中的“vitest react testing library”
搜索:E2E模式的“playwright e2e test login”
测试命名约定
// 模式:it('应该 [预期行为] 当 [条件]')
it('当密码无效时应该显示错误消息')
it('当登录成功时应该重定向到仪表板')
it('当表单正在提交时应该禁用提交按钮')
面试黄金
“我实施了全面的测试,覆盖85%的关键用户流程。我使用Vitest进行单元测试,React Testing Library进行组件集成测试,Playwright进行端到端测试,覆盖登录、结账和支付流程。”
测试是面试的谈资。你写的每个测试都是你理解代码的证明。