Vitest 是构建在 Vite 之上的下一代前端单元测试框架,主打秒级冷启动、原生 ESM、与 Jest API 高度兼容。对于已经用 Vite 做构建的 Vue、React 项目来说,它几乎可以零成本替换 Jest,让“写测试”这件事不再卡在动辄几十秒的启动等待上。本文从安装配置到 Vue/React 组件测试、覆盖率统计、Mock 隔离与 CI 集成,手把手带你把 Vitest 落地到真实工程,让前端单元测试真正变成日常习惯而非负担。
一、为什么还要一个新的测试框架
在 Vitest 出现之前,前端单元测试几乎是 Jest 的代名词。Jest 确实成熟稳定,但在 Vite 已成为主流构建工具的今天,它有几个绕不开的痛点:第一,Jest 默认走 Babel 转译,与 Vite 的 esbuild/原生 ESM 管线重复且不一致,项目里要维护两套转换配置;第二,冷启动慢,大型项目跑单个测试也要等很久;第三,对 TypeScript、ESM 模块、Vue/React SFC 的原生支持都需要额外胶水。Vitest 直接复用 Vite 的配置、解析器和插件生态,测试文件就是普通的模块,import 什么就用什么,没有“测试专用”的魔法。如果你正在用 Vite 做工程化(参见我们的《Vite 工程化实战》),那么接入 Vitest 几乎是顺理成章的一步。
更现实的好处是开发体验:Vitest 的 watch 模式能像 Vite 的 HMR 一样智能重跑受影响的用例,改一行代码立刻看到对应测试的结果,反馈回路从“秒级”压缩到“瞬时”。这对于 TDD(测试驱动开发)和重构安全网尤其关键。
二、Vitest 与 Jest 怎么选
两者 API 几乎一致(describe / it / expect / beforeEach 等),迁移成本极低。下表从工程视角给出对比,帮助你判断当前项目是否适合切到 Vitest。也欢迎回看我们之前写的《前端单元测试实战:用 Jest 与 RTL 写好组件测试》,理解 Jest 的传统用法。
| 维度 | Jest | Vitest |
|---|---|---|
| 冷启动速度 | 较慢(Babel 转译 + 独立运行时) | 秒级(复用 Vite/esbuild) |
| 配置体系 | 独立 jest.config,与构建割裂 | 直接复用 vite.config,零割裂 |
| TS / ESM | 需 ts-jest / babel 额外配置 | 原生支持,开箱即用 |
| Vue / React SFC | 需 vue-jest 等转换插件 | 借助 Vite 插件天然支持 |
| 生态成熟度 | 极高,资料最多 | 快速成长,核心场景已齐备 |
| 迁移成本 | — | 极低,API 兼容 Jest |
结论:新项目、Vite 项目、追求快反馈的团队,直接选 Vitest;历史包袱重、强依赖 Jest 专属生态(如某些自定义 runner)的老项目,可以暂缓。
三、5 分钟快速上手
3.1 安装
在已有的 Vite 项目里,只需一条命令装好依赖:
npm i -D vitest
# 若需组件测试,再装对应工具
npm i -D @vue/test-utils # Vue 3
npm i -D @testing-library/react @testing-library/jest-dom # React
3.2 配置 vite.config.ts
Vitest 的配置就写在 vite.config.ts 的 test 字段里,无需独立文件。environment 决定测试运行环境:纯逻辑用 node,涉及 DOM 用 jsdom(需额外安装 jsdom)。
import { defineConfig } from 'vite'
export default defineConfig({
test: {
environment: 'node', // 纯函数/工具用 node;组件测试改 'jsdom'
globals: true, // 全局注入 describe/it/expect,无需 import
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8', // 或 'istanbul'
reporter: ['text', 'html'],
},
},
})
3.3 配置 npm scripts
{
"scripts": {
"test": "vitest", // watch 模式
"test:run": "vitest run", // 单次运行(CI 用)
"coverage": "vitest run --coverage"
}
}
配好之后,终端敲 npm test 即可进入 watch 模式,敲 npm run test:run 则一次性跑完所有用例,适合接持续集成。
四、编写第一个测试用例
4.1 测试纯函数
假设我们有一个加和工具函数,对应写一个名为 sum.test.ts 的测试文件。globals 开启后,describe/it/expect 已是全局可用。
// src/sum.ts
export function sum(a: number, b: number): number {
return a + b
}
// src/sum.test.ts
import { describe, it, expect } from 'vitest'
import { sum } from './sum'
describe('sum', () => {
it('adds two positive numbers', () => {
expect(sum(1, 2)).toBe(3)
})
it('handles negative numbers', () => {
expect(sum(-1, -2)).toBe(-3)
})
})
4.2 测试 Vue 3 组件
Vue 组件测试借助 @vue/test-utils 的 mount 完成。下面用一个点击会自增的计数器演示,顺便呼应我们讲过的《Pinia 实战:Vue 3 类型安全的状态管理》与《Vue3 组合式 API + Pinia》——组件里的状态逻辑同样可以被测试覆盖。
// Counter.vue
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">count is {{ count }}</button>
</template>
// Counter.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import Counter from './Counter.vue'
describe('Counter', () => {
it('increments on click', async () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('count is 0')
await wrapper.find('button').trigger('click')
expect(wrapper.text()).toContain('count is 1')
})
})
4.3 测试 React 组件
React 侧用 @testing-library/react 的 render + screen + fireEvent 组合。它强调“以用户视角测试”,不直接断言组件内部状态,而是断言渲染出的文本与交互结果,符合我们《React 19 实战》里强调的可维护组件思路。
// Counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import Counter from './Counter'
describe('Counter', () => {
it('increments on click', () => {
render(<Counter />)
const btn = screen.getByText(/count is 0/i)
fireEvent.click(btn)
expect(screen.getByText(/count is 1/i)).toBeInTheDocument()
})
})
五、覆盖率与质量门禁
测试不是写得多就好,覆盖率能客观反映“代码被测到了多少”。Vitest 内置 v8 与 istanbul 两种覆盖率引擎,v8 更快、对 ESM 更友好。建议在 CI 里加上阈值门禁,低于目标就报错,防止覆盖率悄悄下滑。
// vite.config.ts test 字段补充
coverage: {
provider: 'v8',
reportsDirectory: './coverage',
thresholds: {
lines: 80,
functions: 80,
branches: 70,
statements: 80,
},
}
本地执行 npm run coverage 会同时输出终端文本报告与可点击的 HTML 详情页(coverage/index.html),能直观看到哪一行没被覆盖。
六、Mock 与 Spy:隔离外部依赖
真实代码常依赖接口请求、定时器或第三方模块。Vitest 的 vi 命名空间提供了 fn / spyOn / mock 三件套,把外部依赖“替身化”,让测试聚焦于当前逻辑。
import { describe, it, expect, vi, beforeEach } from 'vitest'
// 1) 模拟函数:断言它被调用
const fn = vi.fn().mockReturnValue(42)
expect(fn()).toBe(42)
expect(fn).toHaveBeenCalledOnce()
// 2) 监听已有对象方法
const user = { save: () => 'ok' }
const spy = vi.spyOn(user, 'save')
user.save()
expect(spy).toHaveBeenCalled()
// 3) 自动 mock 整个模块
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Tom' }),
}))
注意 vi.useFakeTimers() 可伪造 setTimeout/setInterval,测试防抖、轮询类逻辑时非常有用;用完记得 vi.useRealTimers() 还原。
七、接入 GitHub Actions 持续集成
测试最大的价值在于“每次提交都自动跑”。把 vitest run 放进 CI,配合缓存能大幅提升速度。下面是一份最小可用的工作流,和我们《GitHub Actions 实战》讲的理念一致。
name: test
on: [push, pull_request]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run test:run -- --coverage
- run: npm run build
PR 里一旦有人改坏逻辑,CI 立刻变红,把问题挡在合并之前——这正是测试作为“安全网”的意义。
八、常见误区与排障清单
初次落地 Vitest 时,几个高频坑几乎人人都会踩。对照下表快速定位:
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 报“environment jsdom is not installed” | 组件测试但未装 jsdom | npm i -D jsdom,并把 environment 改为 ‘jsdom’ |
| describe/it 报 undefined | 未开 globals 又没 import | vite.config 设 globals:true,或显式 import |
| ESM 依赖报 transform 错误 | 依赖未被 Vite 预编译 | 在 test.server.deps.inline 加入该包 |
| 覆盖率始终为 0 | 未安装 coverage provider | npm i -D @vitest/coverage-v8 并设 provider:’v8′ |
| watch 模式卡住不退出 | CI 误用了 watch | CI 用 vitest run,不要直接用 vitest |
| 类型报错不影响测试 | 仅运行时断言 | 配合 tsc –noEmit 做类型门禁更稳 |
另外,编辑器侧推荐装好对应插件(参考《2026 年最值得装的 10 个 VS Code 插件》),在侧边栏直接点绿勾运行单测,体验会再上一个台阶。
九、结语
Vitest 用“复用 Vite、零配置心智、秒级反馈”这三张牌,几乎重新定义了前端单元测试的体验。它不要求你推翻既有工程,只是把“写测试”的成本压到了最低——这正是让团队愿意持续写测试的关键。建议从今天起,给项目里任意一个纯函数补一份 .test.ts,跑通第一条绿色用例,再逐步把组件、关键业务路径纳入覆盖。当测试成为肌肉记忆,重构和升级(比如我们讲过的 Vite 构建优化、React 19 迁移)都会从容得多。




