前端组件测试实战:Vitest 与 Testing Library
前端组件怎么测?Vitest 写单元测试,Testing Library 渲染组件模拟用户操作,覆盖率报告看哪些代码没测到。本文从环境搭建到测试策略,讲清前端组件测试的完整流程。
一、为什么前端需要测试
前端改一个按钮样式,结果整个页面崩了?没有测试,每次改代码都是手动点一遍页面。自动化测试让你改完代码跑一遍测试就知道有没有破坏功能。
| 测试类型 | 测什么 | 工具 |
|---|---|---|
| 单元测试 | 函数、工具类 | Vitest / Jest |
| 组件测试 | UI 组件渲染与交互 | Testing Library |
| E2E 测试 | 完整用户流程 | Playwright / Cypress |
二、搭建 Vitest 环境
# 安装
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
# vitest.config.js
import { defineConfig } from 'vite'
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.js',
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
},
},
})
# package.json 加脚本
# "test": "vitest",
# "test:coverage": "vitest --coverage"
Vitest vs Jest:Vitest 是 Vite 原生的测试框架,速度更快、配置更简单、和 Vite 项目无缝集成。新项目推荐用 Vitest,老项目用 Jest 也没问题。
三、第一个组件测试
// Button.jsx
export function Button({ label, onClick }) {
return <button onClick={onClick}>{label}</button>
}
// Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
test('按钮显示正确的文字', () => {
render(<Button label="提交" />)
expect(screen.getByText('提交')).toBeInTheDocument()
})
test('点击按钮触发回调', () => {
const handleClick = vi.fn()
render(<Button label="提交" onClick={handleClick} />)
fireEvent.click(screen.getByText('提交'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
四、异步组件测试
// UserProfile.jsx
import { useState, useEffect } from 'react'
export function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data)
setLoading(false)
})
}, [userId])
if (loading) return <div>加载中...</div>
return <div>{user.name}</div>
}
// UserProfile.test.jsx
import { render, screen } from '@testing-library/react'
import { UserProfile } from './UserProfile'
// Mock fetch
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: '小明' }),
})
)
test('加载并显示用户名', async () => {
render(<UserProfile userId="1" />)
// 先显示加载中
expect(screen.getByText('加载中...')).toBeInTheDocument()
// 等待异步完成
expect(await screen.findByText('小明')).toBeInTheDocument()
})
五、测试覆盖率
# 运行覆盖率报告
npm run test:coverage
# 输出示例:
# File % Stmts % Branch % Funcs % Lines
# All files 85% 70% 90% 85%
# Button.jsx 100% 100% 100% 100%
# UserProfile 75% 50% 80% 75%
覆盖率不是越高越好,80% 左右是合理目标。重点测核心业务逻辑和边界情况,不要为了覆盖率写无意义的测试。
六、测试最佳实践
按用户行为写测试:不要测试组件内部状态,测试用户看到什么、点什么。
Mock 外部依赖:API 请求、定时器、路由都 mock 掉,测试只关注组件本身。
一个测试只测一件事:不要在一个 test 里写多个断言场景,保持测试清晰。
用 getBy 优先:Testing Library 的查询优先级:getByRole > getByLabelText > getByText > getByTestId。
七、总结
前端测试的核心是 Vitest + Testing Library 的组合。Vitest 跑测试,Testing Library 渲染组件模拟用户操作。覆盖率看核心逻辑覆盖,不追求 100%。记住:测试是为了让你改代码时心里有底,不是为了写测试而写测试。