外观
前端测试体系:Vitest + Playwright 分层测试
"要不要写测试"已经不是问题,真正的问题是:在哪一层写、测什么、怎么让测试长期可维护。
前端测试常见两个极端:一是只写快照测试,改个 class 就大面积失败;二是所有用例都走端到端,跑一次二十分钟,失败后还定位不到原因。合理的做法是分层,把不同风险交给最合适的工具。
分层模型
传统测试金字塔强调大量单元测试、少量端到端测试。前端更接近"测试奖杯":
text
E2E(Playwright) 少量,覆盖关键用户路径
集成测试(Testing Library) 主力,验证组件行为
单元测试(Vitest) 纯逻辑、工具函数、组合函数- 单元测试:不依赖 DOM 和网络,测纯函数、状态转换、数据格式化。
- 集成测试:渲染组件,模拟用户操作,验证可访问性树和交互结果。
- 端到端测试:启动真实应用,覆盖登录、下单、支付等关键链路。
原则是:能用低层测试覆盖的风险,不要放到高层。端到端测试成本高、速度慢、稳定性差,只留给必须跨系统验证的流程。
Vitest:单元与组件测试
Vitest 复用 Vite 的配置和转换链路,启动快,API 与 Jest 基本兼容。
ts
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
restoreMocks: true,
coverage: {
provider: 'v8',
reporter: ['text', 'lcov']
}
}
})纯函数测试:
ts
import { describe, expect, it } from 'vitest'
import { calculateDiscount } from './pricing'
describe('calculateDiscount', () => {
it('满 100 减 20', () => {
expect(calculateDiscount(120)).toBe(20)
})
it('不足门槛不打折', () => {
expect(calculateDiscount(80)).toBe(0)
})
})组件测试优先使用 Testing Library,通过角色、标签和可见文本查询,而不是依赖 class 或内部状态:
ts
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { expect, it } from 'vitest'
import Counter from './Counter.vue'
it('点击按钮后数字增加', async () => {
render(Counter)
const button = screen.getByRole('button', { name: '增加' })
await userEvent.click(button)
expect(screen.getByText('1')).toBeTruthy()
})getByRole 同时验证了可访问性。如果按钮没有可读名称,测试会失败,这恰好能暴露无障碍问题。
用 MSW 管理网络
不要在业务测试里到处 mock fetch。MSW(Mock Service Worker)在请求层拦截,组件代码不需要感知测试环境:
ts
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
import { afterAll, afterEach, beforeAll, expect, it } from 'vitest'
import { render, screen } from '@testing-library/vue'
import ArticleList from './ArticleList.vue'
const server = setupServer(
http.get('/api/articles', () =>
HttpResponse.json([{ id: '1', title: '现代 CSS' }])
)
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
it('渲染文章列表', async () => {
render(ArticleList)
expect(await screen.findByText('现代 CSS')).toBeTruthy()
})MSW 的好处是同一套 handler 可以复用到开发环境和 Storybook 中,测试与真实请求路径保持一致。
Playwright:关键路径的端到端保障
端到端测试只覆盖少量高价值流程,例如登录、搜索、结算、发布。用例要像用户一样操作:
ts
import { expect, test } from '@playwright/test'
test('登录后进入工作台', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('邮箱').fill('dev@example.com')
await page.getByLabel('密码').fill('secret')
await page.getByRole('button', { name: '登录' }).click()
await expect(page).toHaveURL('/dashboard')
await expect(page.getByRole('heading', { name: '工作台' })).toBeVisible()
})配置中处理好本地服务和失败诊断:
ts
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
use: {
baseURL: 'http://localhost:4173',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
webServer: {
command: 'npm run preview',
port: 4173,
reuseExistingServer: !process.env.CI
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } }
]
})trace: 'on-first-retry' 会在重试时保留完整操作轨迹,失败后可以直接回放,比截图有效得多。
测试什么,不测试什么
值得测:
- 纯逻辑和边界条件:金额计算、日期处理、权限判断。
- 组件的用户可见行为:加载、空状态、错误态、键盘操作。
- 关键业务流程:登录、下单、支付回调。
- 曾经出过 bug 的路径:回归测试是最好的防复发手段。
不值得测:
- 第三方库的内部实现。
- 纯样式和 DOM 结构细节。
- 组件内部的私有方法。
- 频繁变动的文案(除非用角色和语义查询)。
常见坑
- 追求覆盖率数字。覆盖率高不代表断言有效,关键是覆盖关键路径和边界。
- 测试实现细节。断言组件内部状态或 DOM 结构,重构时大量误报。
- 滥用快照。大快照没人 review,更新时一键通过,等于没测。
- 端到端测试依赖真实第三方。支付、短信等外部服务应使用测试环境或 mock,避免不稳定。
- 测试之间共享状态。数据库、localStorage、登录态要按用例隔离,否则顺序变化就失败。
- 没有失败诊断。没有 trace、截图、错误日志的 E2E 测试,排查成本极高。
小结
测试体系的目标不是"测试数量",而是用最低成本获得对关键风险的信心。纯逻辑交给 Vitest,组件行为交给 Testing Library,跨系统链路交给 Playwright;再配合 CI 分层执行,才能既保证质量又不拖慢交付。