Skip to content

前端测试体系: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 结构细节。
  • 组件内部的私有方法。
  • 频繁变动的文案(除非用角色和语义查询)。

常见坑 ​

  1. 追求覆盖率数字。覆盖率高不代表断言有效,关键是覆盖关键路径和边界。
  2. 测试实现细节。断言组件内部状态或 DOM 结构,重构时大量误报。
  3. 滥用快照。大快照没人 review,更新时一键通过,等于没测。
  4. 端到端测试依赖真实第三方。支付、短信等外部服务应使用测试环境或 mock,避免不稳定。
  5. 测试之间共享状态。数据库、localStorage、登录态要按用例隔离,否则顺序变化就失败。
  6. 没有失败诊断。没有 trace、截图、错误日志的 E2E 测试,排查成本极高。

小结 ​

测试体系的目标不是"测试数量",而是用最低成本获得对关键风险的信心。纯逻辑交给 Vitest,组件行为交给 Testing Library,跨系统链路交给 Playwright;再配合 CI 分层执行,才能既保证质量又不拖慢交付。

基于 VitePress 构建 · 持续更新中