Vitest 已经成为前端测试的事实标准之一。4.0 版本在快照测试、组件测试和并行执行方面都有重要改进,让测试体验更加流畅。
快照测试的演进
快照测试从"争议性特性"变成了"实用工具",关键在于正确的使用姿势:
typescript
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/vue'
import UserCard from './UserCard.vue'
describe('UserCard', () => {
it('matches snapshot for normal user', () => {
const { container } = render(UserCard, {
props: {
user: {
name: '张三',
avatar: '/avatars/zhangsan.jpg',
role: 'developer'
}
}
})
expect(container).toMatchSnapshot()
})
it('matches snapshot for admin user', () => {
const { container } = render(UserCard, {
props: {
user: {
name: '李四',
avatar: '/avatars/lisi.jpg',
role: 'admin'
}
}
})
expect(container).toMatchSnapshot()
})
})
快照测试的最佳实践
- 小型快照优先:只快照关键 UI 片段,不要快照整个页面
- 有意义的快照名称:使用
toMatchSnapshot({ customSnapshotIdentifiers })添加上下文 - 定期审查快照:不要无脑接受所有快照更新
typescript
// 好的做法:小型、有意义的快照
it('renders loading state correctly', () => {
const { container } = render(Button, { props: { loading: true } })
expect(container.querySelector('.loading-spinner')).toMatchSnapshot()
})
// 不好的做法:大型、模糊的快照
it('renders correctly', () => {
const { container } = render(ComplexPage)
expect(container).toMatchSnapshot() // 快照太大,难以维护
})
组件测试策略
测试层次划分
typescript
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import LoginForm from './LoginForm.vue'
describe('LoginForm', () => {
// 第一层:纯渲染测试
it('renders all form fields', () => {
const wrapper = mount(LoginForm)
expect(wrapper.find('input[name="email"]').exists()).toBe(true)
expect(wrapper.find('input[name="password"]').exists()).toBe(true)
expect(wrapper.find('button[type="submit"]').exists()).toBe(true)
})
// 第二层:交互测试
it('emits submit event with form data', async () => {
const wrapper = mount(LoginForm)
await wrapper.find('input[name="email"]').setValue('test@example.com')
await wrapper.find('input[name="password"]').setValue('password123')
await wrapper.find('form').trigger('submit')
expect(wrapper.emitted('submit')).toHaveLength(1)
expect(wrapper.emitted('submit')[0]).toEqual([{
email: 'test@example.com',
password: 'password123'
}])
})
// 第三层:验证测试
it('shows validation error for invalid email', async () => {
const wrapper = mount(LoginForm)
await wrapper.find('input[name="email"]').setValue('invalid-email')
await wrapper.find('form').trigger('submit')
expect(wrapper.find('.error-message').text()).toContain('邮箱格式不正确')
})
})
Mock 策略
typescript
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount } from '@vue/test-utils'
import UserProfile from './UserProfile.vue'
import { useUser } from '@/composables/useUser'
vi.mock('@/composables/useUser')
describe('UserProfile', () => {
const mockUser = {
id: '1',
name: '张三',
email: 'zhangsan@example.com',
avatar: '/avatars/zhangsan.jpg'
}
beforeEach(() => {
vi.mocked(useUser).mockReturnValue({
user: ref(mockUser),
loading: ref(false),
error: ref(null),
updateUser: vi.fn()
})
})
it('displays user information', () => {
const wrapper = mount(UserProfile)
expect(wrapper.find('.user-name').text()).toBe('张三')
expect(wrapper.find('.user-email').text()).toBe('zhangsan@example.com')
})
it('calls updateUser when form is submitted', async () => {
const wrapper = mount(UserProfile)
const { updateUser } = vi.mocked(useUser())
await wrapper.find('input[name="name"]').setValue('李四')
await wrapper.find('form').trigger('submit')
expect(updateUser).toHaveBeenCalledWith({ name: '李四' })
})
})
并行测试优化
Vitest 4.0 的并行测试性能得到了显著提升:
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 并行测试配置
threads: true,
maxThreads: 4,
minThreads: 2,
// 测试文件隔离
isolate: true,
// 测试超时
testTimeout: 10000,
hookTimeout: 10000,
// 重试机制
retry: 2
}
})
并行测试的注意事项
- 避免共享状态:每个测试应该独立,不依赖其他测试的状态
- 使用 beforeEach 清理:在每个测试前重置状态
- 合理控制并行度:过多的并行可能导致资源竞争
typescript
describe('Database Tests', () => {
let db
beforeEach(async () => {
// 每个测试前创建新的数据库连接
db = await createTestDatabase()
})
afterEach(async () => {
// 每个测试后清理
await db.cleanup()
})
it('creates user', async () => {
const user = await db.createUser({ name: '张三' })
expect(user.id).toBeDefined()
})
it('finds user by id', async () => {
const created = await db.createUser({ name: '李四' })
const found = await db.findUser(created.id)
expect(found.name).toBe('李四')
})
})
快照测试与组件测试的结合
typescript
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/vue'
import DataTable from './DataTable.vue'
describe('DataTable', () => {
const columns = [
{ key: 'name', title: '姓名' },
{ key: 'age', title: '年龄' },
{ key: 'email', title: '邮箱' }
]
const data = [
{ id: 1, name: '张三', age: 25, email: 'zhangsan@example.com' },
{ id: 2, name: '李四', age: 30, email: 'lisi@example.com' }
]
it('renders table with correct structure', () => {
const { container } = render(DataTable, {
props: { columns, data }
})
// 结构验证
expect(container.querySelector('table')).toBeTruthy()
expect(container.querySelectorAll('th')).toHaveLength(3)
expect(container.querySelectorAll('tbody tr')).toHaveLength(2)
// 快照验证视觉样式
expect(container.querySelector('thead')).toMatchSnapshot()
})
it('handles empty data gracefully', () => {
const { container } = render(DataTable, {
props: { columns, data: [] }
})
expect(container.querySelector('.empty-state')).toBeTruthy()
expect(container.querySelector('.empty-state').textContent).toContain('暂无数据')
})
})
测试覆盖率策略
typescript
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/types/',
'*.config.*'
],
thresholds: {
statements: 80,
branches: 75,
functions: 80,
lines: 80
}
}
}
})
覆盖率的最佳实践
- 关注关键路径:核心业务逻辑需要高覆盖率
- 避免为了覆盖率而测试:测试应该验证行为,而不是追求数字
- 使用覆盖率指导补充测试:低覆盖率的地方可能是测试遗漏
typescript
// 好的做法:针对关键业务逻辑
describe('PaymentService', () => {
it('calculates total with tax correctly', () => {
const service = new PaymentService()
const total = service.calculateTotal(100, 0.1)
expect(total).toBe(110)
})
it('handles discount correctly', () => {
const service = new PaymentService()
const total = service.calculateTotal(100, 0.1, 0.2)
expect(total).toBe(88) // 100 * 1.1 * 0.8
})
})
// 不好的做法:为了覆盖率而测试
it('has calculateTotal method', () => {
const service = new PaymentService()
expect(typeof service.calculateTotal).toBe('function')
})
小结
Vitest 4.0 让前端测试变得更加快速和可靠。快照测试的关键是"小而精",组件测试的关键是"层次分明",并行测试的关键是"隔离状态"。在实际项目中,合理的测试策略比追求 100% 覆盖率更重要——测试是为了验证行为,而不是为了满足数字指标。
