Vitest 上手:给 Vue 3 项目补上单元测试
上周重构一个核心组件,改完手动回归花了两小时,心里默念了八遍"要是有测试就好了"。这次痛下决心把单元测试补上,选型没什么悬念——项目用 Vite,测试自然是 Vitest。
为什么不是 Jest
Jest 统治了很多年,但在 Vite 项目里它有两个别扭:
- 项目用 ESM + Vite 的转换管线,Jest 要用自己的一套(babel/ts-jest),同一套代码两套转换逻辑,配置双倍、行为可能不一致
- Jest 对原生 ESM 的支持一直磕磕绊绊
Vitest 直接复用 Vite 的转换和配置,API 还兼容 Jest——迁移成本几乎为零。
十分钟接入
npm i -D vitest @vue/test-utils jsdom
vite.config.ts 里加 test 配置:
/// <reference types="vitest" />
import { defineConfig } from 'vite'
export default defineConfig({
// ...原有配置原封不动,Vitest 直接复用
test: {
environment: 'jsdom', // 组件测试需要 DOM 环境
globals: true, // 不用 import 就能用 describe/it/expect
},
})
{
"scripts": {
"test": "vitest", // watch 模式,改代码自动重跑
"test:run": "vitest run", // CI 用,跑完退出
"coverage": "vitest run --coverage"
}
}
启动速度是真快——冷启动一秒内,这就是原生 ESM + esbuild 的威力。
第一个测试:纯函数
从最简单的开始,测一个工具函数:
// utils/format.test.ts
import { describe, it, expect } from 'vitest'
import { formatPrice } from './format'
describe('formatPrice', () => {
it('分转元,保留两位小数', () => {
expect(formatPrice(12345)).toBe('123.45')
})
it('处理 0 和负数', () => {
expect(formatPrice(0)).toBe('0.00')
expect(formatPrice(-500)).toBe('-5.00')
})
it('非法输入抛错', () => {
expect(() => formatPrice(NaN)).toThrow()
})
})
describe 分组、it 写用例、expect 断言——和 Jest 一模一样的 API。
组件测试:@vue/test-utils
测一个计数器组件:
// components/Counter.test.ts
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter', () => {
it('点击按钮计数加一', async () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('0')
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('1')
})
it('接受初始值 prop', () => {
const wrapper = mount(Counter, {
props: { initial: 10 },
})
expect(wrapper.text()).toContain('10')
})
})
注意 trigger 后要 await——DOM 更新是异步的,这是新手最常忘的点。
外部依赖:mock 掉
组件里调了接口怎么办?mock:
import { vi } from 'vitest'
import { fetchUsers } from '@/api'
// 整个模块 mock
vi.mock('@/api', () => ({
fetchUsers: vi.fn(),
}))
it('加载用户列表', async () => {
// 控制 mock 的返回值
vi.mocked(fetchUsers).mockResolvedValue([
{ id: 1, name: '张三' },
])
const wrapper = mount(UserList)
await flushPromises()
expect(wrapper.text()).toContain('张三')
expect(fetchUsers).toHaveBeenCalledOnce()
})
vi.fn() 造间谍函数、vi.mock() 替换整个模块、vi.mocked() 拿回类型——测试三件套。
我的经验:测试写在哪个性价比最高
不是所有代码都值得测。我的优先级排序:
- 工具函数、数据转换(format、validator)——纯函数,写测试成本最低收益最高,必须测
- 核心 composables(useCart、useForm)——业务逻辑集中地
- 复杂组件的关键交互——按钮点了状态对不对、表单校验通不通
- 简单展示组件——基本不测,靠类型和 review 保证
别追求覆盖率数字。我们项目定的是"核心业务逻辑 80%,全项目不强求"。为了冲 100% 给一行 getter 写测试,纯属自我感动。
CI 集成
GitHub Actions 加一步:
- run: npm run test:run
测试挂了直接拦下 PR。从"烧香式重构"变成"绿灯式重构",安全感完全不同。
小结
- Vite 项目选 Vitest:复用配置、秒级启动、API 兼容 Jest
- 组件测试用 @vue/test-utils,trigger 后记得 await
- 外部依赖 vi.mock 掉,数据请求绝不真的发
- 测试性价比排序:工具函数 > composables > 复杂组件交互 > 展示组件
- 覆盖率看核心逻辑,别追数字
补上测试后第一周,CI 就拦住了一个边界条件的回归 bug。那一刻我深刻理解了什么叫"测试是重构的许可证"。
看到这里了?如果这篇文章帮到过你
请我喝瓶可乐就好 🥤

评论0 条
还没有评论,来抢沙发~