Vitest 单元测试:Vite 原生的快速测试框架

FreeGuideOnline 最新 2026-06-15

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 可以控制 setTimeoutsetInterval 等定时函数,避免实际等待。

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 -uvitest --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.tsxsrc/components/Button.test.tsx
  • 集中目录:在 src 外创建 __tests__ 集中管理,适合单元测试与集成测试分离的场景。

Vitest 会自动检测文件名中包含 .test..spec. 的文件,并在配置中通过 includeexclude 精确控制。

配置参考速查

这里列出常用的 test 配置项及其作用,方便按需查阅。

配置项 类型 说明
globals boolean 全局注册 describe / it / expect
environment string 测试环境,如 nodejsdomhappy-dom
include string[] 测试文件匹配模式
exclude string[] 忽略的测试文件
setupFiles string[] 测试启动前执行的配置文件
coverage CoverageOptions 代码覆盖率配置

更多高级选项请查阅Vitest 官方文档

迁移 Jest 项目

对于现有的 Jest 项目,迁移到 Vitest 通常非常顺畅。主要步骤:

  1. 安装 vitest 并移除 jest 相关依赖。
  2. jest.config.js 调整为 vitest.config.ts,修改映射规则。
  3. 更改测试脚本为 vitest
  4. 替换 Jest 特有的全局 API 或 mock 行为(例如 jest.fn()vi.fn())。

由于 Vitest 的 API 与 Jest 高度兼容,并且提供了 vitest 的别名导出(如 vi 相当于 jest),大部分代码无需改动即可运行。常见的差异点集中在模拟和定时器实现上,Vitest 官方提供了详细的迁移指南。

小结

Vitest 凭借与 Vite 的原生融合、极速的热更新、全面的 Jest 兼容性,已经成为现代前端项目中单元测试与组件测试的标杆工具。它大幅降低了测试配置的成本,让开发者能够专注于编写高质量的测试代码。通过本文的讲解,你已经掌握了 Vitest 的核心用法,包括测试编写、断言、模拟、快照和组件测试,可以立即在项目中将它们付诸实践。