Vitest 是 Vite 生态的现代单元测试框架—— 与 Jest API 兼容,速度快 10 倍以上。 Claude Code 能快速生成覆盖率高、可维护的 Vitest 测试。
为什么选 Vitest?
Vitest vs Jest:
速度:Vitest 利用 Vite 的 ESM 原生支持,快 5-10 倍
配置:零配置,和 Vite 项目天然集成
API:与 Jest 完全兼容,迁移成本极低
TypeScript:原生支持,无需 ts-jest
HMR:文件变更时只重跑受影响的测试
安装配置
bash
npm install -D vitest @vitest/ui @testing-library/vue
# 或 React 项目:
npm install -D vitest @vitest/ui @testing-library/react jsdomtypescript
// vite.config.ts
import { defineConfig } from 'vite'
import { defineConfig as defineVitest } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // 不需要 import { describe, it, expect }
environment: 'jsdom', // 模拟浏览器环境
setupFiles: ['./tests/setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
thresholds: { lines: 80, functions: 80 }
}
}
})场景一:让 Claude Code 生成工具函数测试
Prompt:
帮我为 src/utils/format.ts 里的所有工具函数
写完整的 Vitest 单元测试:
- 正常输入的预期输出
- 边界条件(空值/null/undefined/极端值)
- 异常情况(应该抛出错误的场景)
覆盖率目标 > 90%
假设 format.ts 包含:
typescript
// src/utils/format.ts
export function formatCurrency(amount: number, currency = 'CNY'): string {
if (amount < 0) throw new Error('Amount cannot be negative')
return new Intl.NumberFormat('zh-CN', {
style: 'currency', currency
}).format(amount)
}
export function truncate(str: string, maxLen: number): string {
if (!str) return ''
return str.length <= maxLen ? str : str.slice(0, maxLen) + '...'
}Claude Code 生成的测试:
typescript
// tests/utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatCurrency, truncate } from '../../src/utils/format'
describe('formatCurrency', () => {
it('正确格式化整数金额', () => {
expect(formatCurrency(1000)).toBe('¥1,000.00')
})
it('正确格式化小数金额', () => {
expect(formatCurrency(99.9)).toBe('¥99.90')
})
it('支持其他货币', () => {
expect(formatCurrency(100, 'USD')).toContain('$')
})
it('金额为零时返回零', () => {
expect(formatCurrency(0)).toBe('¥0.00')
})
it('负数金额抛出错误', () => {
expect(() => formatCurrency(-1)).toThrow('Amount cannot be negative')
})
})
describe('truncate', () => {
it('短字符串不截断', () => {
expect(truncate('hello', 10)).toBe('hello')
})
it('超长字符串截断并加省略号', () => {
expect(truncate('hello world', 5)).toBe('hello...')
})
it('恰好等于最大长度不截断', () => {
expect(truncate('hello', 5)).toBe('hello')
})
it('空字符串返回空字符串', () => {
expect(truncate('', 10)).toBe('')
})
it('null/undefined 返回空字符串', () => {
expect(truncate(null as any, 10)).toBe('')
expect(truncate(undefined as any, 10)).toBe('')
})
})场景二:Vue 组件测试
Prompt:
帮我给这个 UserCard.vue 组件写 Vitest 测试:
- 测试 Props 渲染(name/avatar/role)
- 测试 click 事件 emit
- 测试 loading 状态下显示骨架屏
- 测试 error 状态下显示错误提示
typescript
// tests/components/UserCard.test.ts
import { describe, it, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import UserCard from '../../src/components/UserCard.vue'
describe('UserCard', () => {
const defaultProps = {
name: '张三',
avatar: 'https://example.com/avatar.jpg',
role: '管理员'
}
it('正确渲染用户信息', () => {
const wrapper = mount(UserCard, { props: defaultProps })
expect(wrapper.text()).toContain('张三')
expect(wrapper.text()).toContain('管理员')
expect(wrapper.find('img').attributes('src')).toBe(defaultProps.avatar)
})
it('点击卡片触发 select emit', async () => {
const wrapper = mount(UserCard, { props: defaultProps })
await wrapper.trigger('click')
expect(wrapper.emitted('select')).toHaveLength(1)
expect(wrapper.emitted('select')?.[0]).toEqual([defaultProps])
})
it('loading 状态显示骨架屏', () => {
const wrapper = mount(UserCard, {
props: { ...defaultProps, loading: true }
})
expect(wrapper.find('[data-testid="skeleton"]').exists()).toBe(true)
expect(wrapper.find('[data-testid="user-name"]').exists()).toBe(false)
})
})场景三:Pinia Store 测试
Prompt:
帮我给购物车 Pinia Store 写完整测试,
包括 addItem / removeItem / totalPrice 计算属性
typescript
import { describe, it, expect, beforeEach } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useCartStore } from '../../src/stores/cart'
describe('useCartStore', () => {
beforeEach(() => {
setActivePinia(createPinia()) // 每次测试重置 Store
})
it('初始状态购物车为空', () => {
const cart = useCartStore()
expect(cart.items).toHaveLength(0)
expect(cart.totalPrice).toBe(0)
})
it('添加商品', () => {
const cart = useCartStore()
cart.addItem({ id: 1, name: 'iPhone', price: 5999 })
expect(cart.items).toHaveLength(1)
expect(cart.items[0].quantity).toBe(1)
})
it('重复添加同一商品增加数量', () => {
const cart = useCartStore()
cart.addItem({ id: 1, name: 'iPhone', price: 5999 })
cart.addItem({ id: 1, name: 'iPhone', price: 5999 })
expect(cart.items).toHaveLength(1)
expect(cart.items[0].quantity).toBe(2)
})
it('正确计算总价', () => {
const cart = useCartStore()
cart.addItem({ id: 1, name: 'A', price: 100 })
cart.addItem({ id: 1, name: 'A', price: 100 }) // quantity = 2
cart.addItem({ id: 2, name: 'B', price: 200 })
expect(cart.totalPrice).toBe(400)
})
})覆盖率报告
bash
# 运行测试并生成覆盖率报告
npx vitest run --coverage
# 打开可视化报告
npx vitest --ui
# CI 中强制覆盖率门槛
npx vitest run --coverage --coverage.thresholds.lines=80高效 Prompt 模板
[批量生成测试]
分析 src/utils/ 目录下所有工具函数,
为每个函数生成完整的 Vitest 测试,
覆盖正常/边界/异常三类情况,目标覆盖率 90%。
[迁移 Jest 测试]
把这个 Jest 测试文件迁移到 Vitest,
保持测试逻辑不变,使用 Vitest 的最佳实践:
[粘贴 Jest 测试文件]
[测试 async 函数]
这个异步函数会调用外部 API,
帮我用 Vitest 的 vi.mock 模拟 API,
写完整的测试(成功/失败/超时三种情况):
[粘贴函数代码]
来源:Vitest 官方文档 vitest.dev + Anthropic Claude Code 文档