Vitest 单元测试:Vite 原生的快速测试框架
Vitest 简介
Vitest 是由 Vite 团队开发的新一代前端测试框架,它原生支持 Vite 的配置文件与转换管道,无需额外配置即可获得极快的测试启动和热更新体验。Vitest 与 Jest 的 API 高度兼容,可以无缝迁移绝大多数 Jest 测试套件,同时提供了更现代化的 ESM 支持和 TypeScript 开箱即用能力。
对于已经使用 Vite 开发项目的团队,Vitest 是单元测试和组件测试的首选方案。它充分利用 Vite 的预构建依赖、esbuild 编译和 HMR 机制,让测试驱动开发(TDD)流程变得异常流畅。
环境准备
安装 Vitest
在 Vite 项目中,推荐将 Vitest 安装为开发依赖:
npm install -D vitest
如果项目尚未集成 Vite,也可以单独使用 Vitest,但通常建议与 Vite 搭配以获得最佳体验。
创建基础配置
Vitest 会复用 Vite 的配置文件 vite.config.ts,只需在其中添加 test 字段即可启用测试功能。在项目根目录找到或创建 vite.config.ts:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 指定测试文件匹配规则
include: ['**/*.{test,spec}.{js,ts,tsx}'],
// 测试环境,默认为 node,组件测试需设置为 jsdom 或 happy-dom
environment: 'jsdom',
// 全局测试 API,开启后测试文件中无需导入 describe、it 等
globals: true
}
})
如果你更倾向于独立配置文件,可以创建 vitest.config.ts,Vitest 会优先读取它。
设置测试脚本
在 package.json 中添加以下脚本:
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage"
}
}
npm test:启动测试开发模式(监听文件变化自动重新运行)npm run test:ui:启动可视化测试界面(需安装@vitest/ui)npm run coverage:运行一次测试并生成覆盖率报告
编写第一个测试
假设你有一个简单的工具函数 sum,文件路径 src/utils/sum.ts:
export function sum(a: number, b: number): number {
return a + b
}
在同一目录下创建测试文件 sum.test.ts:
import { sum } from './sum'
// 如果配置中开启了 globals: true,describe 和 it 无需手动导入
// 否则需从 vitest 导入:
// import { describe, it, expect } from 'vitest'
describe('sum', () => {
it('应该正确计算两个正数之和', () => {
expect(sum(1, 2)).toBe(3)
})
it('应该处理负数', () => {
expect(sum(-1, -2)).toBe(-3)
})
it('零值不会影响结果', () => {
expect(sum(0, 5)).toBe(5)
})
})
运行 npm test,Vitest 会自动发现并执行测试,终端将显示实时的测试结果。如果开启了 globals: true,所有测试 API 都会挂载在全局作用域,无需显式导入。
核心断言方法
Vitest 兼容 Jest 的断言风格,提供了丰富的匹配器(Matchers)。
常用匹配器
import { describe, it, expect } from 'vitest'
describe('断言示例', () => {
it('精确相等', () => {
expect(2 + 2).toBe(4)
// 对象深度相等使用 toEqual
expect({ name: 'vitest' }).toEqual({ name: 'vitest' })
})
it('真值相关', () => {
expect(1).toBeTruthy()
expect(0).toBeFalsy()
expect(null).toBeNull()
expect(undefined).toBeUndefined()
expect('value').toBeDefined()
})
it('数字比较', () => {
expect(100).toBeGreaterThan(50)
expect(3.14).toBeCloseTo(3.14, 2)
})
it('字符串匹配', () => {
expect('Hello Vitest').toMatch(/vitest/i)
expect('foobar').toContain('foo')
})
it('数组与可迭代对象', () => {
expect([1, 2, 3]).toContain(2)
expect([1, 2, 3]).toHaveLength(3)
})
it('异常', () => {
const throwError = () => { throw new Error('出错了') }
expect(throwError).toThrow('出错了')
// 也可以检查具体类型
expect(throwError).toThrowError(Error)
})
})
自定义匹配器
Vitest 支持通过 expect.extend 扩展自定义匹配器,这在重复的验证场景中非常实用。
测试生命周期钩子
Vitest 提供了与 Jest 几乎一致的生命周期钩子来管理测试前后的准备与清理工作。
import { beforeAll, beforeEach, afterEach, afterAll } from 'vitest'
describe('生命周期', () => {
beforeAll(() => {
// 所有测试开始前执行一次
console.log('初始化数据库连接')
})
afterAll(() => {
// 所有测试结束后执行一次
console.log('关闭数据库连接')
})
beforeEach(() => {
// 每个测试用例执行前运行
console.log('准备测试数据')
})
afterEach(() => {
// 每个测试用例执行后运行
console.log('清理临时数据')
})
it('示例测试', () => {
expect(true).toBe(true)
})
})
钩子的作用域限制在 describe 块内,内层可以访问外层的钩子,执行顺序为:外层 beforeAll → 外层 beforeEach → 内层 beforeEach → 测试 → 内层 afterEach → 外层 afterEach。
模拟与桩
模拟(Mock)是单元测试的核心技术之一,用于隔离被测代码的外部依赖。Vitest 提供了强大的模拟工具。
函数模拟
import { describe, it, expect, vi } from 'vitest'
describe('函数模拟', () => {
it('使用 vi.fn 创建模拟函数', () => {
const mockFn = vi.fn()
mockFn('hello')
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledWith('hello')
})
it('控制返回值', () => {
const mockFn = vi.fn().mockReturnValue('默认值')
expect(mockFn()).toBe('默认值')
// 仅一次调用返回自定义值
mockFn.mockReturnValueOnce('第一次')
expect(mockFn()).toBe('第一次')
expect(mockFn()).toBe('默认值')
})
it('模拟异步函数', () => {
const mockFn = vi.fn().mockResolvedValue('异步结果')
await expect(mockFn()).resolves.toBe('异步结果')
})
})
模块模拟
当被测模块依赖其他模块时,可以使用 vi.mock 整体替换目标模块。
import { describe, it, expect, vi } from 'vitest'
import { fetchUser } from './userService'
import axios from 'axios'
// 模拟整个 axios 模块
vi.mock('axios')
describe('fetchUser', () => {
it('应该返回用户数据', async () => {
const mockUser = { id: 1, name: 'John' }
// 为 axios.get 设置模拟实现
(axios.get as any).mockResolvedValue({ data: mockUser })
const result = await fetchUser(1)
expect(result).toEqual(mockUser)
expect(axios.get).toHaveBeenCalledWith('/users/1')
})
})
Vitest 默认使用 vi.mock 提升到文件顶部执行,以保证模拟在导入之前生效。如果希望动态导入或条件模拟,可以使用 vi.importMock 等方法。
定时器模拟
使用 vi.useFakeTimers 可以控制 setTimeout、setInterval 等定时函数,避免实际等待。
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('定时器模拟', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('快速推进定时器', () => {
const callback = vi.fn()
setTimeout(callback, 1000)
// 立即将时间推进1000ms
vi.advanceTimersByTime(1000)
expect(callback).toHaveBeenCalled()
})
})
快照测试
快照测试用于记录组件的渲染输出或数据结构,并在后续测试中自动对比变化。Vitest 内置了快照支持。
import { describe, it, expect } from 'vitest'
describe('快照测试', () => {
it('对象快照', () => {
const config = {
user: 'admin',
permissions: ['read', 'write'],
version: 1
}
expect(config).toMatchSnapshot()
})
})
首次运行时,Vitest 会在 __snapshots__ 目录下生成 .snap 文件。当被测数据发生变化时,测试会失败并显示差异。若变更是预期内的,可以通过添加 -u 参数更新快照:vitest -u 或 vitest --update。
内联快照(toMatchInlineSnapshot)则会将快照内容直接嵌入测试文件,适合小型快照。
组件测试
配合 @testing-library/vue、@testing-library/react 等工具,Vitest 可以轻松进行前端组件测试。这里以 Vue 组件为例。
安装依赖
npm install -D @testing-library/vue @vue/test-utils jsdom
确保 vite.config.ts 中的 test.environment 设置为 'jsdom'。
测试计数器组件
假设有一个简单的计数器组件 Counter.vue:
<template>
<div>
<span data-testid="count">{{ count }}</span>
<button @click="increment">增加</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value++
}
</script>
测试文件 Counter.test.ts:
import { describe, it, expect } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/vue'
import Counter from './Counter.vue'
describe('Counter', () => {
it('初始值为0', () => {
render(Counter)
expect(screen.getByTestId('count').textContent).toBe('0')
})
it('点击按钮后数值增加', async () => {
render(Counter)
const button = screen.getByText('增加')
await fireEvent.click(button)
expect(screen.getByTestId('count').textContent).toBe('1')
})
})
Vitest + Testing Library 的组合让组件测试的编写贴近真实用户行为,同时保持了极高的运行速度。
代码覆盖率
Vitest 内置了代码覆盖率生成能力,默认使用 V8 覆盖率引擎,也可以切换为 Istanbul。
在配置文件中开启:
export default defineConfig({
test: {
coverage: {
provider: 'v8', // 或 'istanbul'
reporter: ['text', 'json', 'html'],
reportsDirectory: './coverage'
}
}
})
运行 npm run coverage 后,终端会输出覆盖率摘要,同时在 coverage 目录生成 HTML 报告,方便直观查看未覆盖的代码分支。
测试文件组织
大型项目中,合理的测试文件结构能提升可维护性。推荐方案:
- 就近放置:将测试文件与源文件放在同一目录,例如
src/components/Button.tsx→src/components/Button.test.tsx。 - 集中目录:在
src外创建__tests__集中管理,适合单元测试与集成测试分离的场景。
Vitest 会自动检测文件名中包含 .test. 或 .spec. 的文件,并在配置中通过 include 和 exclude 精确控制。
配置参考速查
这里列出常用的 test 配置项及其作用,方便按需查阅。
| 配置项 | 类型 | 说明 |
|---|---|---|
globals |
boolean |
全局注册 describe / it / expect 等 |
environment |
string |
测试环境,如 node、jsdom、happy-dom |
include |
string[] |
测试文件匹配模式 |
exclude |
string[] |
忽略的测试文件 |
setupFiles |
string[] |
测试启动前执行的配置文件 |
coverage |
CoverageOptions |
代码覆盖率配置 |
更多高级选项请查阅Vitest 官方文档。
迁移 Jest 项目
对于现有的 Jest 项目,迁移到 Vitest 通常非常顺畅。主要步骤:
- 安装
vitest并移除jest相关依赖。 - 将
jest.config.js调整为vitest.config.ts,修改映射规则。 - 更改测试脚本为
vitest。 - 替换 Jest 特有的全局 API 或 mock 行为(例如
jest.fn()→vi.fn())。
由于 Vitest 的 API 与 Jest 高度兼容,并且提供了 vitest 的别名导出(如 vi 相当于 jest),大部分代码无需改动即可运行。常见的差异点集中在模拟和定时器实现上,Vitest 官方提供了详细的迁移指南。
小结
Vitest 凭借与 Vite 的原生融合、极速的热更新、全面的 Jest 兼容性,已经成为现代前端项目中单元测试与组件测试的标杆工具。它大幅降低了测试配置的成本,让开发者能够专注于编写高质量的测试代码。通过本文的讲解,你已经掌握了 Vitest 的核心用法,包括测试编写、断言、模拟、快照和组件测试,可以立即在项目中将它们付诸实践。