测试工具几年一换,2024 后的主流:
- 单元测试 / 组件测试:Vitest(替代 Jest,与 Vite 一体)
- E2E 测试:Playwright(替代 Cypress,多浏览器 + 并发)
- 可视化组件库 / VRT:Storybook + Chromatic(可选)
Vitest 安装
npm i -D vitest @vitest/ui jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
vitest.config.ts:
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./src/test/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
},
},
})
src/test/setup.ts:
import '@testing-library/jest-dom/vitest'
package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}
第一个组件测试
// Counter.tsx
import { useState } from 'react'
export function Counter() {
const [n, setN] = useState(0)
return <button onClick={() => setN(n + 1)}>count: {n}</button>
}
// Counter.test.tsx
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { Counter } from './Counter'
test('increments on click', async () => {
render(<Counter />)
expect(screen.getByRole('button')).toHaveTextContent('count: 0')
await userEvent.click(screen.getByRole('button'))
expect(screen.getByRole('button')).toHaveTextContent('count: 1')
})
注意:
screen.getByRole优先于getByTestId(更符合用户视角)userEvent比fireEvent更真实(模拟键盘 / 焦点)
query 优先级
按 testing-library 官方推荐:
getByRole(button, link, heading)getByLabelText(form fields)getByText(text content)getByDisplayValue(input current value)getByAltText/getByTitlegetByTestId(最后选项)
mock 模块 / 函数
import { vi } from 'vitest'
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}))
// 或单个函数
const spy = vi.spyOn(console, 'log').mockImplementation(() => {})
测异步
import { render, screen, waitFor } from '@testing-library/react'
test('loads user', async () => {
render(<UserCard id="1" />)
expect(screen.getByText(/loading/i)).toBeInTheDocument()
await waitFor(() => {
expect(screen.getByText('Alice')).toBeInTheDocument()
})
})
// 或者用 findBy (自动 await)
const el = await screen.findByText('Alice')
MSW:mock 网络请求
npm i -D msw
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/api/user/:id', () => HttpResponse.json({ id: '1', name: 'Alice' })),
http.post('/api/login', async () => HttpResponse.json({ token: 'x' })),
]
// setup.ts
import { setupServer } from 'msw/node'
import { handlers } from '../mocks/handlers'
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
测试代码里直接调真实 fetch,MSW 在网络层拦截返回 mock 数据。
比 mock fetch 函数干净得多。
Playwright 安装
npm init playwright@latest
# 选项:TypeScript / 是否要 GitHub Actions / 是否装浏览器
生成的目录:
tests/
example.spec.ts
playwright.config.ts
第一个 E2E
// tests/login.spec.ts
import { test, expect } from '@playwright/test'
test('user can log in', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('[email protected]')
await page.getByLabel('Password').fill('secret')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page).toHaveURL(/\/dashboard/)
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible()
})
跑:
npx playwright test
npx playwright test --ui # 交互模式(推荐开发用)
npx playwright test --debug # 单步调试
多浏览器
playwright.config.ts:
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
]
npx playwright test --project=mobile 只跑特定 project。
自动等待 / locator
Playwright 的 locator 自带 auto-wait:
await page.getByText('Save').click()
// 等元素出现 + 可点击 + 视口内 → click
不需要写 waitForSelector / sleep。
截图 / 视频 / trace(调试神器)
// playwright.config.ts
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
}
测试失败时:
npx playwright show-report
# 浏览器打开 HTML 报告,看到失败时的截图 / 视频 / 一帧帧的 DOM 快照
trace viewer 让你"时间穿越"看测试每一步页面状态——比 console.log 强 100 倍。
CI 集成
# .github/workflows/test.yml
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run test:unit
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
性能基准
- 单元测试:每个 ~10ms,1000 个测试几秒跑完
- Playwright:每个 1-5s(浏览器启动 + 实际操作),用
workers: 4并发
Vitest vs Jest
Vitest 优势:
- 与 Vite 共享配置(plugins / alias)
- 启动快得多(< 1s 冷启 vs Jest 几秒)
- ESM-native
- Watch 模式更快(Vite 增量 build)
迁移:API 几乎与 Jest 兼容,jest.fn() → vi.fn()。
踩过的坑
import.meta.env在 Vitest 里要在 setup 里 mock:测试时 env 是 test。- Playwright 测真 API 时数据库被污染:用
test.beforeEach清表,或单独
test environment + Docker 起独立 DB。 - userEvent 漏
await:操作没真发生测试通过,假绿。await userEvent.click(...)
必须 await。 - Playwright 第一次跑要下载浏览器(~300MB),CI 用
--with-deps
装系统 lib 否则跑不起来。
登录后参与评论。