Vitest 实战:Vite 原生前端单元测试框架

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 的传统用法。

维度JestVitest
冷启动速度较慢(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”组件测试但未装 jsdomnpm i -D jsdom,并把 environment 改为 ‘jsdom’
describe/it 报 undefined未开 globals 又没 importvite.config 设 globals:true,或显式 import
ESM 依赖报 transform 错误依赖未被 Vite 预编译在 test.server.deps.inline 加入该包
覆盖率始终为 0未安装 coverage providernpm i -D @vitest/coverage-v8 并设 provider:’v8′
watch 模式卡住不退出CI 误用了 watchCI 用 vitest run,不要直接用 vitest
类型报错不影响测试仅运行时断言配合 tsc –noEmit 做类型门禁更稳

另外,编辑器侧推荐装好对应插件(参考《2026 年最值得装的 10 个 VS Code 插件》),在侧边栏直接点绿勾运行单测,体验会再上一个台阶。

九、结语

Vitest 用“复用 Vite、零配置心智、秒级反馈”这三张牌,几乎重新定义了前端单元测试的体验。它不要求你推翻既有工程,只是把“写测试”的成本压到了最低——这正是让团队愿意持续写测试的关键。建议从今天起,给项目里任意一个纯函数补一份 .test.ts,跑通第一条绿色用例,再逐步把组件、关键业务路径纳入覆盖。当测试成为肌肉记忆,重构和升级(比如我们讲过的 Vite 构建优化、React 19 迁移)都会从容得多。

上一篇 PostgreSQL 流复制实战:用 Patroni 实现高可用主从集群
下一篇 KubeVirt 实战:用 Kubernetes 统一编排容器与虚拟机
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]