跳到主要内容

前端测试: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()

查询优先级通常为:getByRolegetByLabelText、文本,最后才是 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 保护
  • 失败产物足以快速诊断
  • 覆盖率用于发现盲区,而非成为唯一目标