测试策略技能
目标
使用现代工具(Vitest, Playwright)实施全面的测试策略,涵盖单元、集成和E2E测试,并设定清晰的覆盖目标和最佳实践。
何时使用此技能
自动调用当:
- 用户提到“测试”、“测试”、“覆盖”、“TDD”、“E2E”
- 设置新项目
- 添加新功能(需要测试)
- 调试测试失败
- 提高测试覆盖率
测试金字塔
/\
/E2E\ 少量,慢,昂贵
/------\
/ 集成 \ 一些,中等速度
/----------\
/ 单元测试 \ 许多,快速,便宜
/--------------\
分布:
- 70% 单元测试 - 快速,隔离,便宜
- 20% 集成测试 - 中等速度,测试交互
- 10% E2E测试 - 慢,昂贵,关键用户流程
测试类型
1. 单元测试(Vitest)
是什么:测试隔离中的单个函数/组件
工具:Vitest, React Testing Library
覆盖目标:80%+
设置:
npm install -D vitest @vitest/ui @testing-library/react @testing-library/jest-dom
配置(vitest.config.ts):
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'tests/'],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80
}
}
}
})
示例(Button.test.tsx):
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { Button } from './Button'
describe('Button', () => {
it('渲染文本', () => {
render(<Button>点击我</Button>)
expect(screen.getByText('点击我')).toBeInTheDocument()
})
it('点击时调用onClick', () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>点击</Button>)
fireEvent.click(screen.getByText('点击'))
expect(handleClick).toHaveBeenCalledOnce()
})
it('当disabled属性为true时禁用', () => {
render(<Button disabled>禁用</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
})
命令:
npm run test # 运行所有测试
npm run test:watch # 监视模式
npm run test:ui # 可视化UI
npm run test:coverage # 带覆盖率
2. 集成测试(Vitest)
是什么:测试组件交互、API调用、状态管理
示例(UserProfile.test.tsx):
import { render, screen, waitFor } from '@testing-library/react'
import { describe, it, expect, vi } from 'vitest'
import { UserProfile } from './UserProfile'
// 模拟API
vi.mock('./api', () => ({
fetchUser: vi.fn(() => Promise.resolve({
id: 1,
name: 'John Doe',
email: 'john@example.com'
}))
}))
describe('UserProfile集成', () => {
it('获取并显示用户数据', async () => {
render(<UserProfile userId="1" />)
expect(screen.getByText('加载中...')).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('john@example.com')).toBeInTheDocument()
})
})
})
3. E2E测试(Playwright)
是什么:在真实浏览器中测试完整的用户流程
工具:Playwright
覆盖目标:仅关键路径
设置:
npm install -D @playwright/test
npx playwright install
配置(playwright.config.ts):
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'mobile',
use: { ...devices['iPhone 13'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
示例(e2e/auth.spec.ts):
import { test, expect } from '@playwright/test'
test.describe('认证流程', () => {
test('用户可以注册并登录', async ({ page }) => {
// 注册
await page.goto('/signup')
await page.fill('[name="email"]', 'test@example.com')
await page.fill('[name="password"]', 'SecurePass123!')
await page.click('button[type="submit"]')
// 应重定向到仪表板
await expect(page).toHaveURL(/\/dashboard/)
await expect(page.locator('h1')).toContainText('欢迎')
// 登出
await page.click('[aria-label="用户菜单"]')
await page.click('text=登出')
// 应重定向到首页
await expect(page).toHaveURL('/')
// 重新登录
await page.goto('/login')
await page.fill('[name="email"]', 'test@example.com')
await page.fill('[name="password"]', 'SecurePass123!')
await page.click('button[type="submit"]')
await expect(page).toHaveURL(/\/dashboard/)
})
})
命令:
npx playwright test # 运行所有E2E
npx playwright test --ui # 交互模式
npx playwright test --headed # 显示浏览器
npx playwright test --project=chromium # 特定浏览器
npx playwright show-report # 查看最后报告
测试最佳实践
AAA模式
// 安排
const user = { id: 1, name: 'John' }
const mockFetch = vi.fn()
// 行动
const result = await fetchUser(mockFetch, 1)
// 断言
expect(result).toEqual(user)
expect(mockFetch).toHaveBeenCalledWith('/api/users/1')
测试命名
// 好的:描述性,解释什么和何时
it('当API返回404时显示错误消息', () => {})
it('当表单无效时禁用提交按钮', () => {})
// 坏的:模糊,不清晰
it('有效', () => {})
it('测试1', () => {})
每个测试一个断言(指南)
// 优先聚焦测试
it('渲染用户名', () => {
render(<User name="John" />)
expect(screen.getByText('John')).toBeInTheDocument()
})
it('渲染用户邮箱', () => {
render(<User email="john@example.com" />)
expect(screen.getByText('john@example.com')).toBeInTheDocument()
})
// 超过复杂测试
it('渲染用户数据', () => {
// 多个不相关的断言
})
模拟外部依赖
// 模拟API调用
vi.mock('./api', () => ({
fetchUser: vi.fn()
}))
// 模拟环境
vi.stubEnv('API_URL', 'http://test-api.com')
// 模拟计时器
vi.useFakeTimers()
const now = new Date('2024-01-01')
vi.setSystemTime(now)
覆盖策略
测试什么
✅ 要测试:
- 业务逻辑
- 边缘情况和错误处理
- 用户交互
- API集成
- 状态管理
- 验证逻辑
- 关键用户流程(E2E)
❌ 不要测试:
- 第三方库
- 框架内部
- 常量
- 简单的getter/setter
- 生成的代码
覆盖目标
最低:
- 行:80%
- 函数:80%
- 分支:75%
- 语句:80%
理想:
- 关键路径:100%
- 业务逻辑:95%+
- UI组件:85%+
- 工具:90%+
运行覆盖率
npm run test:coverage
# 在浏览器中查看
open coverage/index.html
测试工作流程
1. TDD方法(推荐)
1. 编写失败的测试
2. 编写最小代码通过
3. 重构
4. 重复
2. 测试后(实用)
1. 实现功能
2. 编写测试
3. 实现80%+覆盖率
4. 有信心地重构
3. 提交前测试
# 提交前运行
npm run test:quick # 快速单元测试
npm run lint
npm run typecheck
# 提交前推送
npm run test # 所有单元/集成
npm run test:coverage # 验证覆盖率
# 部署前运行
npm run test:e2e # 完整E2E套件
测试组织
目录结构
src/
├── components/
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx # 共同定位
│ │ └── Button.stories.tsx # Storybook
│ └── ...
tests/
├── setup.ts # 测试设置
├── utils/ # 测试工具
│ ├── renderWithProviders.tsx # 自定义渲染
│ └── mockData.ts # 测试夹具
└── __mocks__/ # 全局模拟
e2e/
├── auth.spec.ts
├── checkout.spec.ts
└── fixtures/ # E2E测试数据
命名约定
- 单元/集成:
*.test.ts或*.test.tsx - E2E:
*.spec.ts - 设置:
setup.ts,vitest.config.ts
持续集成
GitHub Actions示例
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm run test:coverage
- name: 上传覆盖率
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
调试测试
Vitest
# 运行单个测试文件
npm run test -- Button.test.tsx
# 运行匹配模式的测试
npm run test -- --grep "Button渲染"
# 在VS Code中调试
# 添加断点,按F5
Playwright
# 调试模式
npx playwright test --debug
# 特定测试
npx playwright test auth.spec.ts --debug
# 跟踪查看器
npx playwright show-trace trace.zip
常见测试模式
测试异步代码
it('获取用户数据', async () => {
const { result } = renderHook(() => useUser(1))
await waitFor(() => {
expect(result.current.data).toEqual({ id: 1, name: 'John' })
})
})
测试错误状态
it('当获取失败时显示错误', async () => {
vi.mocked(fetchUser).mockRejectedValue(new Error('网络错误'))
render(<UserProfile userId="1" />)
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument()
})
})
测试表单
it('提交有效数据的表单', async () => {
const handleSubmit = vi.fn()
render(<LoginForm onSubmit={handleSubmit} />)
await userEvent.type(screen.getByLabelText('邮箱'), 'test@example.com')
await userEvent.type(screen.getByLabelText('密码'), 'password123')
await userEvent.click(screen.getByRole('button', { name: /提交/i }))
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123'
})
})
与其他技能集成
quality-gates- 将测试作为质量检查git-workflow- 提交前钩子中的测试codebase-analysis- 识别未测试的代码
package.json脚本
{
"scripts": {
"test": "vitest",
"test:watch": "vitest --watch",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed",
"test:all": "npm run test:coverage && npm run test:e2e"
}
}
版本历史
- 1.0.0 (2025-01-03):初始测试策略,使用Vitest和Playwright