前端测试:Vitest、Playwright 与组件测试
🧪 测试的价值是用合理成本建立反馈系统。优先覆盖高风险业务行为,而不是追求脱离上下文的覆盖率数字。
测试分层
- 单元测试:纯函数、状态机、数据转换,运行快且定位精准。
- 组件测试:验证组件交互、状态和可访问语义。
- 集成测试:验证模块、接口和存储协作。
- 端到端测试:从用户视角覆盖关键链路。
理想结构不是固定“金字塔”,应按产品风险、团队速度和系统架构调整。
Vitest
适合 Vite 项目的单元和组件测试,与 Vite 配置、TypeScript 和 ESM 集成良好。
import { describe, expect, it } from 'vitest'
import { calculateTotal } from './cart'
describe('calculateTotal', () => {
it('applies discount', () => {
expect(calculateTotal([{ price: 100, qty: 2 }], 0.1)).toBe(180)
})
})
实践要点:
- 使用 Arrange / Act / Assert 组织测试。
- Mock 系统边界,不要 Mock 被测对象的内部实现。
- 时间、随机数和网络请求应可控。
- 每个测试独立,避免依赖执行顺序。
组件测试
以用户能观察到的行为为中心:文本、角色、标签和交互结果。避免依赖内部 state、私有方法或脆弱 CSS 选择器。
const button = screen.getByRole('button', { name: '保存' })
await user.click(button)
expect(await screen.findByText('保存成功')).toBeVisible()
查询优先级通常为:getByRole、getByLabelText、文本,最后才是 data-testid。
Playwright E2E
import { expect, test } from '@playwright/test'
test('user can create a note', async ({ page }) => {
await page.goto('/notes')
await page.getByRole('button', { name: '新建笔记' }).click()
await page.getByLabel('标题').fill('事件循环')
await page.getByRole('button', { name: '保存' }).click()
await expect(page.getByText('事件循环')).toBeVisible()
})
Playwright 内置自动等待,应避免固定 waitForTimeout。优先等待可观察状态,例如元素可见、URL 变化或响应完成。
测试数据与隔离
- 每个测试创建自己的数据并在结束后清理。
- 使用 API 或 fixture 快速准备前置条件,UI 只验证目标流程。
- 对数据库写入使用唯一标识,支持并行执行。
- 不共享可变账号或依赖测试顺序。
网络策略
- 单元测试中 Mock 网络客户端。
- 组件测试可使用 MSW 模拟真实协议层。
- E2E 的核心链路尽量连接真实后端测试环境;对难以稳定控制的第三方服务做边界替身。
降低不稳定性
- 使用稳定的语义定位器。
- 保持测试数据独立。
- 处理动画和异步状态,不使用随意 sleep。
- 失败时保留 trace、截图、视频和服务日志。
- 重试用于收集证据,不应掩盖持续失败。
CI 建议
- PR:lint、类型检查、单元和关键组件测试。
- 合并后:完整集成与 E2E。
- 定时任务:多浏览器、移动端、视觉回归和慢速链路。
自检清单
- 测试名称描述用户行为和预期结果
- 断言聚焦可观察结果
- 关键业务路径有 E2E 保护
- 失败产物足以快速诊断
- 覆盖率用于发现盲区,而非成为唯一目标