Vue Test Utils实战技巧与常见问题避坑指南
先说结论,我基本只用 Vue Test Utils + Vitest
如果你问我现在测 Vue 组件选哪个方案,我会直接告诉你:Vue Test Utils 配合 Vitest。不是因为它是官方推荐,而是因为——我真的被其他方案折腾够了。
以前项目小的时候,用 Jest 搭 Vue Test Utils 还行。但自从我们项目上了 TypeScript、引入了大量自定义指令和动态组件后,Jest 的启动时间越来越离谱,CI 上跑一次单元测试要快 3 分钟。后来团队里有人提议换 Vitest,我一开始还觉得没必要,结果试了一周,彻底转粉了。
今天就想聊聊我在实际项目中对比过的几种主流组合,重点讲 Vue Test Utils 在不同环境下的表现,以及我踩过的那些坑。
谁更灵活?谁更省事?
Vue Test Utils 本身只是一个测试工具库,它不绑定运行时环境。你可以用它搭配 Jest、Mocha、Vitest 甚至 Karma。但真正影响体验的,其实是背后的测试运行器(test runner)和构建系统。
我主要对比三套组合:
- Jest + Vue Test Utils
- Mocha + @vue/test-utils + webpack
- Vitest + Vue Test Utils
前两个是老派方案,第三个是近两年冒出来的黑马。
Jest:曾经的王者,现在有点重
说实话,Jest 曾经是我首选。配置一次能用半年,社区生态也完善。但问题是——太重了。
特别是当你用了 Vue 3 的 <script setup> 和 TypeScript 后,你需要额外装 ts-jest、babel-jest,还得处理 ESM 模块导入问题。下面是一个典型配置:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\.vue$': '@vue/vue3-jest',
'^.+\.tsx?$': 'ts-jest',
'^.+\.jsx?$': 'babel-jest'
},
moduleFileExtensions: ['js', 'json', 'jsx', 'ts', 'tsx', 'vue'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
}
}
看着挺标准对吧?但这个配置我在 Windows 上就翻过车:路径别名映射出错,报错信息还不清楚。折腾了半天发现是大小写敏感的问题。还有就是热更新慢得像蜗牛,改个测试文件要等 5 秒才跑起来。
而且最烦的是 mock 组件时语法啰嗦:
import { mount } from '@vue/test-utils'
import MyComponent from '@/components/MyComponent.vue'
describe('MyComponent', () => {
it('renders correctly', () => {
const wrapper = mount(MyComponent, {
global: {
mocks: {
$route: { path: '/test' }
},
stubs: ['router-link', 'icon-button']
}
})
expect(wrapper.text()).toContain('Hello Test')
})
})
这种写法没问题,但一旦你要模拟 provide/inject 或者 pinia store,就得写一堆 setup 函数或者提前注册全局插件,非常繁琐。虽然功能全,但不够轻快。
Mocha 老古董?确实不太适合现代 Vue
几年前有些团队还在用 Mocha + karma-webpack 来跑 Vue 测试。说实话,这套我已经多年没碰了,上次是因为接手一个老项目被迫用了一下。
配置复杂到爆炸。你要搞懂 karma 的 launcher、webpack 的 alias、Babel 的 preset,还要手动引入 polyfill 才能让 globalThis 正常工作。跑个简单测试要启动浏览器实例,本地调试延迟感人。
代码倒是差不多:
it('should render title', () => {
const wrapper = mount(MyComponent)
expect(wrapper.find('h1').text()).toBe('Welcome')
})
看起来简洁,但实际上每个测试文件开头都要写一堆 import 和 setup,维护成本高。CI 环境还得装 Chrome Headless,时不时崩溃一下。我现在看到 karma.conf.js 文件都想绕着走。
结论很明确:除非你在维护十年以上的老项目,否则真没必要碰这套组合。
Vitest:丝滑到让我怀疑人生
第一次跑 Vitest 是同事拉我 review 代码时看到的。他随口说了句“你试试看这个,比 Jest 快十倍”,我不信邪,自己搭了个 demo 项目验证了一下——卧槽,真香。
首先安装简单:
npm install -D vitest @vue/test-utils jsdom
然后配个 vite.config.ts 就能跑:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'jsdom',
transformMode: {
web: [/.[tj]sx$/]
}
}
})
注意这里有个关键点:transformMode.web 让 Vitest 只把 Vue 文件交给 Vite 插件处理,JS/TS 文件默认由 esbuild 快速编译,速度起飞。
写测试也特别顺手:
// MyComponent.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import MyComponent from '../src/components/MyComponent.vue'
describe('MyComponent', () => {
it('renders properly', () => {
const wrapper = mount(MyComponent, {
props: {
title: 'Hello Vitest'
}
})
expect(wrapper.text()).includes('Hello Vitest')
expect(wrapper.find('button')).toBeTruthy()
})
})
热重载几乎是即时的,保存即跑,开发体验完全不是一个级别。而且原生支持 ESM、TypeScript、HMR,几乎零配置就能跑起来。
我还试过在测试中调接口:
global.fetch = vi.fn(() =>
Promise.resolve({ json: () => Promise.resolve({ id: 1 }) })
)
// 测试完成后清理
afterEach(() => {
vi.clearAllMocks()
})
配合 vi.fn() 和 vi.spyOn(),mock 起来比 Jest 更自然。而且 Vitest 自带 UI 界面,打开 localhost:3000 就能看到所有测试用例状态,排查失败用例一目了然。
我的选型逻辑
现在新项目我一律推 Vitest + Vue Test Utils。原因很简单:
- 启动速度快,开发时幸福感爆棚
- 配置极简,基本不用动就能跑 TS 和 Vue
- 和 Vite 生态无缝衔接,别名、CSS、assets 都自动识别
- API 兼容 Jest,迁移成本低
至于老项目要不要迁?看情况。如果现有 Jest 跑得好好的,没必要为了“新技术”硬切。但我建议新模块统一用 Vitest 写测试,逐步过渡。
有一个小坑要注意:如果你用了 Cypress Component Testing,它的底层也是基于 Vue Test Utils,但它依赖具体的框架适配器。这时候就不能随便换 runner 了。不过这是另一个话题了。
另外提一句,fetch 模拟这块,我之前在 Jest 里经常忘记清空 mock 导致测试污染,Vitest 的 vi.mock() 和作用域控制做得更好,减少了这类问题。
以上是我的对比总结,有不同看法欢迎评论区交流
我不是说 Jest 不好,只是它已经不适合我对“快速反馈”的要求了。前端这几年变化太快,测试工具也在进化。Vitest 抓住了“开发者体验”这个痛点,赢了。
Vue Test Utils 本身没啥大问题,API 设计合理,文档清晰。关键是它能在不同的 runner 上稳定工作。所以选择哪个方案,本质上是在选背后的支持体系。
我个人观点:不要死守旧技术。哪怕公司流程还没跟上,至少在自己的 demo 或工具库中尝试新方案。不然哪天突然让你优化 CI 时间,你会发现根本无从下手。
最后提醒一点:不管用哪个 runner,记得给你的组件测试加上覆盖率报告。我们现在的配置是:
{
"test": {
"coverage": {
"provider": "istanbul",
"reportsDirectory": "coverage",
"lines": 80,
"functions": 80,
"branches": 70,
"statements": 80
}
}
}
这样至少能避免写出一堆“通过但毫无意义”的测试。
这个技巧的拓展用法还有很多,后续会继续分享这类实战经验。以上是我踩坑后的总结,希望对你有帮助。

暂无评论