跳转至

Vitest 快速单元测试

一句话概述:Vitest 是基于 Vite 的下一代测试框架,比 Jest 快 10 倍以上,原生支持 ESM 和 TypeScript,共享 Vite 配置,零额外配置就能跑测试。

核心知识点

概念白话解释
Vite-native和 Vite 共用同一套模块解析和转换管道
热更新测试文件改了只重跑受影响的测试,像 HMR 一样快
Jest 兼容API 和 Jest 几乎一样,迁移成本极低
浏览器模式可以在真实浏览器里跑组件测试
覆盖率内置 v8 和 istanbul 两种覆盖率方案
工作空间支持 monorepo 多包测试

安装配置

# 安装 Vitest
npm install -D vitest  # 安装为开发依赖(只要装这一个包!)

# 如果需要覆盖率报告
npm install -D @vitest/coverage-v8  # v8 覆盖率(推荐,更快)

# 如果需要 UI 界面
npm install -D @vitest/ui  # 可视化测试界面

配置文件

// vitest.config.ts(或直接写在 vite.config.ts 里)
import { defineConfig } from 'vitest/config'  // 导入 vitest 配置

export default defineConfig({
  test: {
    globals: true,  // 全局导入 describe/it/expect,不用每个文件都 import
    environment: 'jsdom',  // 模拟浏览器环境(测 DOM 用)
    // environment: 'node',  // Node 环境(测后端代码用)
    include: ['src/**/*.{test,spec}.{js,ts,jsx,tsx}'],  // 测试文件匹配模式
    coverage: {
      provider: 'v8',  // 覆盖率引擎
      reporter: ['text', 'html', 'json'],  // 报告格式
      thresholds: {  // 覆盖率阈值
        lines: 80,  // 行覆盖率至少 80%
        branches: 80,  // 分支覆盖率
        functions: 80,  // 函数覆盖率
      },
    },
    setupFiles: ['./src/test/setup.ts'],  // 测试前运行的配置文件
  },
})

package.json 脚本

{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage",
    "test:ui": "vitest --ui"
  }
}

基本使用

编写测试

// src/utils/math.ts - 被测代码
export function add(a: number, b: number): number {
  return a + b  // 加法
}

export function divide(a: number, b: number): number {
  if (b === 0) throw new Error('除数不能为零')  // 处理除零
  return a / b  // 除法
}

export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1)  // 首字母大写
}
// src/utils/math.test.ts - 测试文件
import { describe, it, expect } from 'vitest'  // 导入测试函数
import { add, divide, capitalize } from './math'  // 导入被测函数

describe('add 加法函数', () => {  // describe: 分组
  it('1 + 2 应该等于 3', () => {  // it: 单个测试
    expect(add(1, 2)).toBe(3)  // toBe: 精确等于
  })

  it('负数相加', () => {
    expect(add(-1, -2)).toBe(-3)
  })

  it('零加任何数等于那个数', () => {
    expect(add(0, 5)).toBe(5)
  })
})

describe('divide 除法函数', () => {
  it('10 / 2 应该等于 5', () => {
    expect(divide(10, 2)).toBe(5)
  })

  it('除以零应该抛出错误', () => {
    expect(() => divide(10, 0)).toThrow('除数不能为零')  // 断言抛出特定错误
  })
})

describe('capitalize 首字母大写', () => {
  it('hello 变成 Hello', () => {
    expect(capitalize('hello')).toBe('Hello')
  })
})

运行测试

vitest  # 监听模式(文件改了自动重跑)
vitest run  # 跑一次就退出
vitest run --coverage  # 跑测试并生成覆盖率报告
vitest --ui  # 打开浏览器 UI 界面
vitest -t "加法"  # 只跑名字包含"加法"的测试
vitest src/utils/  # 只跑这个目录的测试

高级用法

Mock 模拟

import { describe, it, expect, vi } from 'vitest'  // vi 是 vitest 的 mock 工具

// Mock 函数
describe('Mock 函数', () => {
  it('追踪函数调用', () => {
    const fn = vi.fn()  // 创建 mock 函数
    fn('hello')  // 调用它
    fn('world')

    expect(fn).toHaveBeenCalledTimes(2)  // 被调用了 2 次
    expect(fn).toHaveBeenCalledWith('hello')  // 曾经用 'hello' 调用过
  })

  it('模拟返回值', () => {
    const fn = vi.fn()
      .mockReturnValueOnce(1)  // 第一次调用返回 1
      .mockReturnValueOnce(2)  // 第二次返回 2
      .mockReturnValue(0)  // 之后都返回 0

    expect(fn()).toBe(1)
    expect(fn()).toBe(2)
    expect(fn()).toBe(0)
  })
})

// Mock 模块
vi.mock('./db', () => ({  // 模拟整个模块
  getUser: vi.fn().mockResolvedValue({ id: 1, name: '张三' }),  // 异步 mock
}))

import { getUser } from './db'  // 导入的是 mock 版本

it('模拟数据库查询', async () => {
  const user = await getUser(1)
  expect(user.name).toBe('张三')
})

异步测试

// 测试异步函数
describe('异步操作', () => {
  it('async/await 方式', async () => {
    const result = await fetchData()  // 等待异步结果
    expect(result).toEqual({ status: 'ok' })  // toEqual: 深度对比对象
  })

  it('Promise 方式', () => {
    return expect(fetchData()).resolves.toEqual({ status: 'ok' })  // resolves: Promise 成功
  })

  it('Promise 失败', () => {
    return expect(fetchBadData()).rejects.toThrow('网络错误')  // rejects: Promise 失败
  })
})

快照测试

it('对象快照', () => {
  const user = { id: 1, name: '张三', email: 'zhang@test.com' }
  expect(user).toMatchSnapshot()  // 第一次运行保存快照,之后对比
})

it('内联快照', () => {
  expect(capitalize('hello')).toMatchInlineSnapshot('"Hello"')  // 快照直接写在测试里
})

参数化测试

import { describe, it, expect } from 'vitest'

// it.each 批量测试不同输入
describe('加法参数化测试', () => {
  it.each([
    [1, 2, 3],      // [输入1, 输入2, 期望结果]
    [0, 0, 0],
    [-1, 1, 0],
    [100, 200, 300],
  ])('add(%i, %i) = %i', (a, b, expected) => {  // %i 是占位符
    expect(add(a, b)).toBe(expected)
  })
})

// 对象形式(更清晰)
describe.each([
  { input: 'hello', expected: 'Hello' },
  { input: 'world', expected: 'World' },
  { input: 'vitest', expected: 'Vitest' },
])('capitalize("$input")', ({ input, expected }) => {
  it(`应该返回 "${expected}"`, () => {
    expect(capitalize(input)).toBe(expected)
  })
})

定时器模拟

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'

describe('定时器测试', () => {
  beforeEach(() => {
    vi.useFakeTimers()  // 启用假定时器
  })

  afterEach(() => {
    vi.restoreAllMocks()  // 恢复真实定时器
  })

  it('延迟函数', () => {
    const callback = vi.fn()
    setTimeout(callback, 1000)  // 1 秒后调用

    vi.advanceTimersByTime(999)  // 快进 999ms
    expect(callback).not.toHaveBeenCalled()  // 还没到时间

    vi.advanceTimersByTime(1)  // 再快进 1ms
    expect(callback).toHaveBeenCalledOnce()  // 现在调用了
  })
})

常见报错

报错信息原因解决方案
Cannot find module模块解析失败检查 vite.config.ts 的 alias 配置
ReferenceError: describe is not defined没开全局模式配置 globals: true 或 import 它们
document is not defined测 DOM 没配环境environment: 'jsdom'
覆盖率全是 0没装覆盖率包npm install -D @vitest/coverage-v8
vi is not defined没导入 viimport { vi } from 'vitest'
Mock 不生效mock 位置不对vi.mock() 必须写在文件顶层

速查表

# CLI 命令
vitest                   # 监听模式
vitest run               # 运行一次
vitest run --coverage    # 带覆盖率
vitest --ui              # UI 界面
vitest -t "测试名"       # 按名称过滤
vitest --reporter=verbose # 详细输出
vitest bench             # 性能基准测试

# 常用断言
expect(x).toBe(y)              # 严格相等(===)
expect(x).toEqual(y)           # 深度对比
expect(x).toBeTruthy()         # 真值
expect(x).toBeFalsy()          # 假值
expect(x).toBeNull()           # null
expect(x).toBeUndefined()      # undefined
expect(x).toBeGreaterThan(y)   # 大于
expect(x).toContain(item)      # 包含(数组/字符串)
expect(fn).toThrow()           # 抛出错误
expect(fn).toHaveBeenCalled()  # 被调用过
expect(x).toMatchSnapshot()    # 快照匹配

# Mock 工具
vi.fn()                  # 创建 mock 函数
vi.mock('./module')      # 模拟整个模块
vi.spyOn(obj, 'method')  # 监视方法调用
vi.useFakeTimers()       # 假定时器
vi.advanceTimersByTime(ms) # 快进时间
vi.restoreAllMocks()     # 恢复所有 mock

参考:Vitest 官网 | GitHub | 入门指南