版本说明
本文档基于 Vitest 3.x 编写,涵盖最新的配置和 API。
核心版本:
- Vitest: v3.2.4 (2024年12月发布)
- @vitest/coverage-v8: v3.2.4
- Vite: v5.0.0+
重要里程碑:
- ✅ Vitest 3.0 (2024年):新 Reporter API、按行号测试、新断言匹配器
- ✅ Vitest 3.2 (2024年12月):性能改进、新配置选项、浏览器模式增强
- ✅ Vitest 4.0 (2025年初):即将发布
主要特性(Vitest 3.x):
- ⚡ 极速启动:基于 Vite 的快速 HMR
- 🔄 智能监听:文件变化自动重新运行相关测试
- 📦 原生 ESM:完整的 ES 模块支持
- 🎯 Jest 兼容:兼容 Jest API,迁移简单
- 🌐 浏览器模式:真实浏览器环境测试
- 🔍 按行号过滤:
vitest foo.test.js:10
注意事项
- Vitest 3.x 与 2.x 相比有少量破坏性变更,建议新项目直接使用 3.x
- 公共 API 重新设计:
vitest/node的公共 API 已重新设计 - 浏览器模式:支持 Playwright 和 WebdriverIO 配置
- 新增断言:
toHaveBeenCalledBefore、toHaveBeenCalledAfter、toBeOneOf、toSatisfy
本文档介绍如何在 monorepo 项目中为工具函数库配置 Vitest 单元测试。
概述 #
Vitest 是一个由 Vite 提供支持的极速单元测试框架,专为现代前端项目设计。它提供了与 Jest 兼容的 API,同时具有更快的启动速度和更好的 ES 模块支持。
项目结构 #
packages/my-app-vite/
├── src/
│ ├── utils1.ts # 工具函数
│ └── utils1.test.ts # 测试文件
├── vitest.config.ts # Vitest 配置文件
└── package.json # 包含测试脚本配置步骤 #
1. 安装依赖 #
在项目根目录的 package.json 中,Vitest 和覆盖率工具已作为开发依赖安装:
{
"devDependencies": {
"vitest": "^3.2.4",
"@vitest/coverage-v8": "^3.2.4"
}
}注意: 在 monorepo 架构中,我们将测试相关依赖安装在根目录,这样所有子包都可以共享这些依赖,避免重复安装。
2. 创建 Vitest 配置文件 #
在 packages/my-app-vite/vitest.config.ts 中创建配置:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 测试环境 (node | jsdom | happy-dom)
environment: 'node',
// 测试文件匹配模式
include: ['src/**/*.{test,spec}.{js,ts}', 'tests/**/*.{test,spec}.{js,ts}'],
// 排除的文件
exclude: ['node_modules', 'dist'],
// 全局设置
globals: true,
// 覆盖率配置
coverage: {
provider: 'v8', // 或 'istanbul'
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'dist/', '**/*.d.ts', '**/*.config.{js,ts}', 'coverage/**'],
},
},
})Vitest 3.x 配置变更
- 已弃用:
environmentMatchGlobs→ 使用projects配置 - 已弃用:
poolMatchGlobs→ 使用projects配置 - 新增:
workspace字段支持 monorepo 内联配置 - 新增:
browser配置支持浏览器环境测试
多项目配置(Vitest 3.x 推荐) #
如果需要为不同测试类型配置不同环境:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 使用 projects 替代已弃用的 environmentMatchGlobs
projects: [
{
extends: true, // 继承根配置
test: {
name: 'unit', // 项目名称
include: ['**/*.unit.test.ts'],
environment: 'node',
},
},
{
extends: true,
test: {
name: 'browser',
include: ['**/*.browser.test.ts'],
environment: 'jsdom', // 或 'happy-dom'
},
},
],
},
})浏览器模式配置(Vitest 3.x 新特性) #
Vitest 3.x 支持在真实浏览器环境中运行测试:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
browser: {
enabled: true,
provider: 'playwright', // 或 'webdriverio'
headless: true, // 无头模式
instances: [
{ browser: 'chromium' },
// 可以配置多个浏览器
// { browser: 'firefox' },
// { browser: 'webkit' },
],
},
},
})初始化浏览器模式依赖:
pnpm exec vitest init browserMonorepo 工作区配置(Vitest 3.x) #
对于 monorepo 项目,可以使用内联 workspace 配置:
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
// 使用 workspace 字段简化 monorepo 配置
workspace: ['packages/*'],
// 或者混合使用 glob 和内联配置
projects: [
'packages/*', // glob 模式
{
extends: true,
test: {
name: 'integration',
include: ['tests/**/*.integration.test.ts'],
environment: 'node',
},
},
],
},
})3. 配置 package.json 脚本 #
在 packages/my-app-vite/package.json 中添加测试脚本:
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:run:verbose": "vitest run --reporter=verbose",
"test:ui": "vitest --ui",
"test:browser": "vitest --browser"
}
}Vitest 3.x CLI 新特性
按行号过滤测试(Vitest 3.0+):
# 运行指定文件中特定行号的测试
vitest src/utils.test.ts:42
# 运行多个行号的测试
vitest src/utils.test.ts:10,src/utils.test.ts:25
# 支持相对路径和绝对路径
vitest ./basic/foo.js:10
vitest /users/project/basic/foo.js:10其他有用的 CLI 选项:
# 运行指定项目(多项目配置时)
vitest --project=unit
# 列出所有测试文件
vitest list --filesOnly
# 禁用 boolean 选项(两种方式)
vitest --no-api
vitest --api=false
# 使用多个 reporter
vitest --reporter=dot --reporter=default测试文件编写 #
基本结构 #
测试文件 src/utils1.test.ts 的基本结构:
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { debounce, throttle, deepClone, generateId } from './utils1'
describe('utils1', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.restoreAllMocks()
vi.useRealTimers()
})
// 测试用例...
})Vitest 常用 API 详解 #
1. 测试结构 API #
describe(name, fn) #
用于创建测试套件,将相关的测试用例分组:
describe('工具函数测试', () => {
// 测试用例...
})
// 嵌套 describe
describe('utils1', () => {
describe('debounce', () => {
// debounce 相关测试
})
describe('throttle', () => {
// throttle 相关测试
})
})it(name, fn) 或 test(name, fn) #
定义单个测试用例:
it('应该返回正确的结果', () => {
// 测试逻辑
})
// 或者使用 test
test('应该返回正确的结果', () => {
// 测试逻辑
})
// 异步测试
it('应该处理异步操作', async () => {
const result = await asyncFunction()
expect(result).toBe('expected')
})2. 断言 API - expect #
基本断言 #
// 相等性断言
expect(actual).toBe(expected) // 严格相等 (===)
expect(actual).toEqual(expected) // 深度相等
expect(actual).not.toBe(expected) // 不相等
// 真值断言
expect(value).toBeTruthy() // 真值
expect(value).toBeFalsy() // 假值
expect(value).toBeNull() // null
expect(value).toBeUndefined() // undefined
expect(value).toBeDefined() // 已定义
// 数值断言
expect(number).toBeGreaterThan(3) // 大于
expect(number).toBeGreaterThanOrEqual(3) // 大于等于
expect(number).toBeLessThan(5) // 小于
expect(number).toBeCloseTo(0.3) // 浮点数近似相等字符串断言 #
expect(string).toMatch(/pattern/) // 正则匹配
expect(string).toContain('substring') // 包含子字符串
expect(string).toHaveLength(5) // 长度数组和对象断言 #
expect(array).toContain(item) // 数组包含元素
expect(array).toHaveLength(3) // 数组长度
expect(object).toHaveProperty('key') // 对象有属性
expect(object).toHaveProperty('key', 'value') // 对象属性值
expect(array).toEqual(expect.arrayContaining([1, 2])) // 数组包含函数断言 #
expect(mockFn).toHaveBeenCalled() // 函数被调用
expect(mockFn).toHaveBeenCalledTimes(2) // 调用次数
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2') // 调用参数
expect(mockFn).toHaveBeenLastCalledWith('arg') // 最后一次调用参数
// Vitest 3.0+ 新增断言
expect(mockFn).toHaveBeenCalledExactlyOnceWith('arg') // 精确调用一次
expect(mockFn1).toHaveBeenCalledBefore(mockFn2) // 调用顺序:之前
expect(mockFn1).toHaveBeenCalledAfter(mockFn2) // 调用顺序:之后
// 异常断言
expect(() => fn()).toThrow() // 抛出异常
expect(() => fn()).toThrow('error message') // 抛出特定异常Vitest 3.x 新增断言示例:
import { describe, it, expect, vi } from 'vitest'
describe('Vitest 3.x 新增断言', () => {
it('应该精确调用一次', () => {
const mockFn = vi.fn()
mockFn('arg1')
// ✅ Vitest 3.0+ 新增:检查是否精确调用一次且参数匹配
expect(mockFn).toHaveBeenCalledExactlyOnceWith('arg1')
})
it('应该验证调用顺序', () => {
const mock1 = vi.fn()
const mock2 = vi.fn()
mock1()
mock2()
mock1()
// ✅ Vitest 3.0+ 新增:验证调用顺序
expect(mock1).toHaveBeenCalledBefore(mock2)
expect(mock2).toHaveBeenCalledAfter(mock1)
})
it('应该匹配多个可能值之一', () => {
const value = 'red'
// ✅ Vitest 3.0+ 新增:匹配多个可能值之一
expect(value).toBeOneOf(['red', 'green', 'blue'])
})
it('应该满足自定义条件', () => {
const num = 42
// ✅ Vitest 3.0+ 新增:自定义断言条件
expect(num).toSatisfy(n => n > 40 && n < 50)
expect([1, 2, 3]).toSatisfy(arr => arr.every(n => n > 0))
})
})异步断言 #
// Promise 断言
await expect(promise).resolves.toBe('value')
await expect(promise).rejects.toThrow('error')
// 或者使用 async/await
it('异步测试', async () => {
const result = await asyncFunction()
expect(result).toBe('expected')
})3. Mock 和 Spy API - vi #
创建 Mock 函数 #
// 创建 mock 函数
const mockFn = vi.fn()
// 带返回值的 mock
const mockFn = vi.fn(() => 'return value')
// 带实现的 mock
const mockFn = vi.fn((a, b) => a + b)
// 检查 mock 调用
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2')
expect(mockFn).toHaveBeenCalledTimes(1)
// ✅ Vitest 3.0+ 新增:精确调用检查
expect(mockFn).toHaveBeenCalledExactlyOnceWith('arg1', 'arg2')Spy 监听 #
// 监听对象方法
const spy = vi.spyOn(console, 'log')
console.log('test')
expect(spy).toHaveBeenCalledWith('test')
// 监听并模拟返回值
const spy = vi.spyOn(Math, 'random').mockReturnValue(0.5)模拟模块 #
Vitest 3.x Mock 重要注意事项
vi.mock 提升行为:
vi.mock()调用会被自动提升到文件顶部,在 import 之前执行- 如需在 mock 中使用外部变量,必须使用
vi.hoisted() vi.doMock()不会被提升,但只影响后续的动态 import
vi.useFakeTimers() 变更(Vitest 3.0+):
- 默认现在会 mock 所有计时器相关 API(包括
performance.now()) - 旧版本中
performance.now()不会被 mock - 如需自定义,在配置中设置
fakeTimers.toFake
// ❌ 错误:外部变量在 vi.mock 中不可用
const mockValue = 100
vi.mock('./utils', () => ({
getValue: () => mockValue, // ❌ undefined
}))
// ✅ 正确:使用 vi.hoisted
const mocks = vi.hoisted(() => ({
getValue: vi.fn(() => 100),
}))
vi.mock('./utils', () => ({
getValue: mocks.getValue,
}))
// ✅ 部分模拟(保留实际实现)
vi.mock('./utils', async () => {
const actual = await vi.importActual('./utils')
return {
...actual,
specificFunction: vi.fn(),
}
})
// ✅ 使用 vi.doMock(非提升)访问外部变量
let mockCounter = 0
vi.doMock('./counter', () => ({
getCount: () => ++mockCounter,
}))
// 注意:vi.doMock 只影响后续的动态 import
const { getCount } = await import('./counter')
getCount() // 1时间控制 #
// 使用假时间(Vitest 3.0+ 默认 mock 所有计时器 API)
vi.useFakeTimers()
// 推进时间
vi.advanceTimersByTime(1000) // 推进 1 秒
vi.advanceTimersToNextTimer() // 推进到下一个定时器
// 恢复真实时间
vi.useRealTimers()
// 设置系统时间
vi.setSystemTime(new Date('2023-01-01'))
// ⚠️ Vitest 3.0+ 注意:performance.now() 现在也会被 mock
vi.useFakeTimers()
performance.now() // 返回假时间
// 如需恢复 Vitest 2.x 行为,在配置文件中设置:
// vitest.config.ts
export default defineConfig({
test: {
fakeTimers: {
toFake: [
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'setImmediate',
'clearImmediate',
'Date',
// 不包括 'performance' 以保持旧行为
],
},
},
})4. 生命周期钩子 #
beforeEach(fn) 和 afterEach(fn) #
在每个测试用例前后执行:
describe('测试套件', () => {
beforeEach(() => {
// 每个测试前执行
vi.useFakeTimers()
})
afterEach(() => {
// 每个测试后执行
vi.restoreAllMocks()
vi.useRealTimers()
})
})beforeAll(fn) 和 afterAll(fn) #
在整个测试套件前后执行:
describe('测试套件', () => {
beforeAll(() => {
// 所有测试前执行一次
// 例如:设置数据库连接
})
afterAll(() => {
// 所有测试后执行一次
// 例如:清理资源
})
})5. 实用工具 #
跳过和仅运行 #
// 跳过测试
it.skip('跳过这个测试', () => {
// 不会执行
})
// 仅运行这个测试
it.only('只运行这个测试', () => {
// 只有这个会执行
})
// 跳过整个套件
describe.skip('跳过的套件', () => {
// 整个套件都不会执行
})条件测试 #
// 根据条件运行测试
it.runIf(process.platform === 'win32')('Windows 专用测试', () => {
// 只在 Windows 上运行
})
// 根据条件跳过测试
it.skipIf(process.env.CI)('本地环境测试', () => {
// 在 CI 环境中跳过
})6. 实际使用示例 #
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
describe('debounce 函数测试', () => {
let mockFn: ReturnType<typeof vi.fn>
beforeEach(() => {
// 每个测试前创建新的 mock 函数和假时间
mockFn = vi.fn()
vi.useFakeTimers()
})
afterEach(() => {
// 每个测试后清理
vi.restoreAllMocks()
vi.useRealTimers()
})
it('应该延迟执行函数', () => {
const debouncedFn = debounce(mockFn, 100)
// 调用防抖函数
debouncedFn('test')
// 立即检查 - 不应该被调用
expect(mockFn).not.toHaveBeenCalled()
// 推进时间
vi.advanceTimersByTime(100)
// 现在应该被调用了
expect(mockFn).toHaveBeenCalledWith('test')
expect(mockFn).toHaveBeenCalledTimes(1)
})
})测试用例示例 #
1. 防抖函数测试 #
describe('debounce', () => {
it('应该延迟执行函数', () => {
const mockFn = vi.fn()
const debouncedFn = debounce(mockFn, 100)
debouncedFn('test')
expect(mockFn).not.toHaveBeenCalled()
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledWith('test')
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('应该在多次调用时只执行最后一次', () => {
const mockFn = vi.fn()
const debouncedFn = debounce(mockFn, 100)
debouncedFn('first')
debouncedFn('second')
debouncedFn('third')
expect(mockFn).not.toHaveBeenCalled()
vi.advanceTimersByTime(100)
expect(mockFn).toHaveBeenCalledWith('third')
expect(mockFn).toHaveBeenCalledTimes(1)
})
})2. 节流函数测试 #
describe('throttle', () => {
it('应该限制函数执行频率', () => {
const mockFn = vi.fn()
const throttledFn = throttle(mockFn, 100)
throttledFn('test1')
expect(mockFn).toHaveBeenCalledWith('test1')
expect(mockFn).toHaveBeenCalledTimes(1)
// 在限制时间内调用不应该执行
throttledFn('test2')
expect(mockFn).toHaveBeenCalledTimes(1)
// 等待限制时间过去
vi.advanceTimersByTime(100)
throttledFn('test3')
expect(mockFn).toHaveBeenCalledWith('test3')
expect(mockFn).toHaveBeenCalledTimes(2)
})
})3. 深拷贝函数测试 #
describe('deepClone', () => {
it('应该克隆基本类型', () => {
expect(deepClone(null)).toBe(null)
expect(deepClone(undefined)).toBe(undefined)
expect(deepClone(42)).toBe(42)
expect(deepClone('string')).toBe('string')
expect(deepClone(true)).toBe(true)
})
it('应该克隆对象', () => {
const obj = {
a: 1,
b: 'string',
c: {
d: 2,
e: [1, 2, 3],
},
}
const clonedObj = deepClone(obj)
expect(clonedObj).toEqual(obj)
expect(clonedObj).not.toBe(obj)
expect(clonedObj.c).not.toBe(obj.c)
expect(clonedObj.c.e).not.toBe(obj.c.e)
})
})4. 类型工具函数测试(utils2.ts) #
类型工具函数主要用于运行时类型检查,这些函数不需要使用假时间,测试相对简单但覆盖面要广:
describe('utils2 - 类型工具函数', () => {
describe('isString', () => {
it('应该正确识别字符串', () => {
expect(isString('hello')).toBe(true)
expect(isString('')).toBe(true)
expect(isString('123')).toBe(true)
expect(isString(`模板字符串`)).toBe(true)
})
it('应该正确识别非字符串', () => {
expect(isString(123)).toBe(false)
expect(isString(true)).toBe(false)
expect(isString(null)).toBe(false)
expect(isString(undefined)).toBe(false)
expect(isString({})).toBe(false)
expect(isString([])).toBe(false)
expect(isString(() => {})).toBe(false)
})
})
describe('isNumber', () => {
it('应该正确识别数字', () => {
expect(isNumber(123)).toBe(true)
expect(isNumber(0)).toBe(true)
expect(isNumber(-123)).toBe(true)
expect(isNumber(3.14)).toBe(true)
expect(isNumber(Infinity)).toBe(true)
expect(isNumber(-Infinity)).toBe(true)
})
it('应该正确识别 NaN 为数字类型', () => {
// 注意:typeof NaN === 'number'
expect(isNumber(NaN)).toBe(true)
})
it('应该正确识别非数字', () => {
expect(isNumber('123')).toBe(false)
expect(isNumber(true)).toBe(false)
expect(isNumber(null)).toBe(false)
expect(isNumber(undefined)).toBe(false)
})
})
describe('isObject', () => {
it('应该正确识别对象', () => {
expect(isObject({})).toBe(true)
expect(isObject({ a: 1 })).toBe(true)
expect(isObject(new Date())).toBe(true)
expect(isObject([])).toBe(true) // 数组也是对象
})
it('应该正确识别 null 为非对象', () => {
// 虽然 typeof null === 'object',但函数正确排除了 null
expect(isObject(null)).toBe(false)
})
})
describe('isArray', () => {
it('应该正确识别数组', () => {
expect(isArray([])).toBe(true)
expect(isArray([1, 2, 3])).toBe(true)
expect(isArray(['a', 'b', 'c'])).toBe(true)
})
it('应该正确识别类数组对象为非数组', () => {
const arrayLike = { 0: 'a', 1: 'b', length: 2 }
expect(isArray(arrayLike)).toBe(false)
})
})
// 类型保护功能测试
describe('类型保护功能', () => {
it('isString 应该提供正确的类型保护', () => {
const value: unknown = 'hello'
if (isString(value)) {
// 在这个分支中,TypeScript 应该知道 value 是 string 类型
expect(value.toUpperCase()).toBe('HELLO')
expect(value.length).toBe(5)
}
})
it('isArray 应该提供正确的类型保护', () => {
const value: unknown = [1, 2, 3]
if (isArray(value)) {
// 在这个分支中,TypeScript 应该知道 value 是数组类型
expect(value.length).toBe(3)
expect(value.push(4)).toBe(4)
}
})
})
// 边界情况测试
describe('边界情况', () => {
it('应该正确处理特殊数值', () => {
expect(isNumber(Number.MAX_VALUE)).toBe(true)
expect(isNumber(Number.MIN_VALUE)).toBe(true)
expect(isNumber(Number.POSITIVE_INFINITY)).toBe(true)
expect(isNumber(Number.NEGATIVE_INFINITY)).toBe(true)
})
it('应该正确处理包装对象', () => {
// 注意:这些是对象,不是原始类型
expect(isObject(new String('test'))).toBe(true)
expect(isObject(new Number(123))).toBe(true)
expect(isObject(new Boolean(true))).toBe(true)
expect(isString(new String('test'))).toBe(false)
expect(isNumber(new Number(123))).toBe(false)
expect(isBoolean(new Boolean(true))).toBe(false)
})
})
})类型工具函数测试特点:
- 全面性测试:每个函数都测试正确识别目标类型和排除其他类型
- 边界情况:测试特殊值如 NaN、Infinity、null、undefined 等
- 类型保护验证:确保函数能正确作为 TypeScript 类型保护使用
- 包装对象处理:测试
new String()等包装对象的特殊情况 - 实际应用场景:验证函数在真实代码中的表现
运行测试 #
本地运行 #
在 packages/my-app-vite 目录下:
1. 开发时使用 - 实时反馈 #
npm run test- 监听模式(Watch Mode)
- 启动后会持续运行,监听文件变化
- 当你修改源代码或测试文件时,会自动重新运行相关测试
- 适合开发阶段使用,提供实时反馈
- 按
q可以退出,按r可以重新运行所有测试 - 提供交互式界面,可以过滤测试、查看覆盖率等
2. 快速验证或CI中使用 - 一次性检查 #
npm run test:run- 一次性运行模式
- 运行所有测试后立即退出
- 不会监听文件变化
- 适合CI/CD 环境或需要快速验证的场景
- 运行完成后返回退出码(0表示成功,非0表示失败)
- 输出简洁的测试结果
3. 代码质量检查 - 查看测试覆盖率 #
npm run test:coverage- 一次性运行 + 代码覆盖率报告
- 在运行测试的同时生成代码覆盖率报告
- 会创建
coverage目录,包含详细的覆盖率数据 - 生成多种格式的报告(text、json、html)
- 适合代码质量检查和发布前验证
- 可以看到哪些代码行被测试覆盖,哪些没有
4. 详细测试输出 - 调试和分析 #
npm run test:run:verbose- 一次性运行 + 详细输出模式
- 显示每个测试套件和测试用例的详细信息
- 输出格式类似于树状结构,便于查看测试层次
- 适合调试测试和分析测试结构时使用
- 比简洁模式提供更多信息,便于定位问题
输出对比:
简洁模式(npm run test:run):
✓ src/utils2.test.ts (33 tests) 7ms
✓ src/utils1.test.ts (18 tests) 13ms
Test Files 2 passed (2)
Tests 51 passed (51)详细模式(npm run test:run:verbose):
✓ src/utils1.test.ts (18)
✓ utils1 (18)
✓ debounce (4)
✓ 应该延迟执行函数
✓ 应该在多次调用时只执行最后一次
✓ 应该在 immediate 为 true 时立即执行
✓ 应该正确处理多个参数
✓ throttle (3)
✓ 应该限制函数执行频率
✓ 应该保持 this 上下文
✓ 应该正确处理多个参数覆盖率报告表头说明 #
运行 npm run test:coverage 后会显示覆盖率表格,表头各列含义如下:
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
utils1.ts | 95.24 | 83.33 | 100 | 94.74 | 23-25
utils2.ts | 100 | 100 | 100 | 100 |
------------|---------|----------|---------|---------|-------------------
All files | 97.62 | 91.67 | 100 | 97.37 |各列详解:
File:被测试的源文件名% Stmts(Statement Coverage - 语句覆盖率)- 被执行的语句占总语句数的百分比
- 计算方式:(执行的语句数 / 总语句数) × 100%
- 最基本的覆盖率指标,确保代码被执行
% Branch(Branch Coverage - 分支覆盖率)- 被测试的分支条件占总分支数的百分比
- 包括:if/else、switch/case、三元操作符、逻辑运算符等
- 示例:
if (x > 0) { ... } else { ... }需要测试两种情况才能达到100%
% Funcs(Function Coverage - 函数覆盖率)- 被调用的函数占总函数数的百分比
- 确保每个函数都至少被执行一次
% Lines(Line Coverage - 行覆盖率)- 被执行的代码行占总代码行数的百分比
- 空行、注释行、声明行通常不计入统计
Uncovered Line #s(未覆盖的行号)- 列出所有未被测试覆盖的具体行号
- 格式:单行
45,连续行23-25,多个区间23-25,45,67-70 - 帮助快速定位需要补充测试的代码位置
覆盖率目标建议:
| 指标 | 一般标准 | 高质量标准 |
|---|---|---|
| 语句覆盖率 | ≥ 80% | ≥ 90% |
| 分支覆盖率 | ≥ 75% | ≥ 85% |
| 函数覆盖率 | ≥ 90% | ≥ 95% |
| 行覆盖率 | ≥ 80% | ≥ 90% |
提高覆盖率的方法:
- 查看未覆盖行号:根据
Uncovered Line #s定位问题代码 - 补充分支测试:确保所有 if/else、switch 分支都被测试
- 测试边界情况:测试函数的各种输入情况和异常情况
- 检查异常处理:确保 try/catch 块被覆盖
在 monorepo 根目录运行 #
# 使用 pnpm workspace 过滤器
pnpm --filter @vue/my-app-vite run test:run测试结果示例 #
运行 npm run test:run 后的完整测试结果:
✓ src/utils1.test.ts (18)
✓ utils1 (18)
✓ debounce (4)
✓ 应该延迟执行函数
✓ 应该在多次调用时只执行最后一次
✓ 应该在 immediate 为 true 时立即执行
✓ 应该正确处理多个参数
✓ throttle (3)
✓ 应该限制函数执行频率
✓ 应该保持 this 上下文
✓ 应该正确处理多个参数
✓ deepClone (6)
✓ 应该克隆基本类型
✓ 应该克隆日期对象
✓ 应该克隆数组
✓ 应该克隆对象
✓ 应该处理嵌套对象
✓ 应该处理包含数组的对象
✓ generateId (5)
✓ 应该生成带有默认前缀的ID
✓ 应该生成带有自定义前缀的ID
✓ 应该生成唯一的ID
✓ 应该生成指定长度的随机部分
✓ 应该只包含字母和数字
✓ src/utils2.test.ts (64)
✓ utils2 - 类型工具函数 (64)
✓ isString (8)
✓ 应该正确识别字符串
✓ 应该正确识别非字符串
✓ isNumber (8)
✓ 应该正确识别数字
✓ 应该正确识别 NaN 为数字类型
✓ 应该正确识别非数字
✓ isBoolean (6)
✓ 应该正确识别布尔值
✓ 应该正确识别非布尔值
✓ isFunction (10)
✓ 应该正确识别函数
✓ 应该正确识别箭头函数
✓ 应该正确识别类构造函数
✓ 应该正确识别非函数
✓ isObject (8)
✓ 应该正确识别对象
✓ 应该正确识别 null 为非对象
✓ 应该正确识别非对象
✓ isArray (8)
✓ 应该正确识别数组
✓ 应该正确识别类数组对象为非数组
✓ 应该正确识别非数组
✓ isUndefined (6)
✓ 应该正确识别 undefined
✓ 应该正确识别未声明的变量属性为 undefined
✓ 应该正确识别非 undefined
✓ isNull (4)
✓ 应该正确识别 null
✓ 应该正确识别非 null
✓ isNullOrUndefined (6)
✓ 应该正确识别 null 或 undefined
✓ 应该正确识别非 null 且非 undefined
Test Files 2 passed (2)
Tests 82 passed (82)
Start at 10:38:15
Duration 425ms (transform 89ms, setup 0ms, collect 67ms, tests 15ms, environment 0ms, prepare 124ms)最佳实践 #
1. 测试文件命名 #
- 测试文件应与源文件同名,添加
.test.ts或.spec.ts后缀 - 例如:
utils1.ts→utils1.test.ts
2. 测试结构 #
- 使用
describe分组相关测试 - 使用
it或test编写具体测试用例 - 测试描述应清晰说明测试目的
3. Mock 和 Spy #
// 使用 vi.fn() 创建 mock 函数
const mockFn = vi.fn()
// 使用 vi.spyOn() 监听对象方法
const spy = vi.spyOn(console, 'log')
// 使用 vi.mock() 模拟整个模块
vi.mock('./module', () => ({
default: vi.fn(),
}))4. 时间相关测试 #
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
// 在测试中控制时间
vi.advanceTimersByTime(1000)5. 异步测试 #
it('应该处理异步操作', async () => {
const result = await asyncFunction()
expect(result).toBe('expected')
})
it('应该处理 Promise 拒绝', async () => {
await expect(asyncFunction()).rejects.toThrow('error message')
})配置选项详解 #
测试环境 #
export default defineConfig({
test: {
environment: 'node', // 'node' | 'jsdom' | 'happy-dom'
},
})全局 API #
export default defineConfig({
test: {
globals: true, // 启用全局 API,无需导入 describe, it, expect
},
})覆盖率配置 #
export default defineConfig({
test: {
coverage: {
provider: 'v8', // 'v8' | 'istanbul'
reporter: ['text', 'json', 'html'],
threshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
},
},
})故障排除 #
常见问题 #
模块解析问题
// 在 vitest.config.ts 中配置路径别名 export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, './src'), }, }, })TypeScript 类型问题
// 在 tsconfig.json 中添加 vitest 类型 { "compilerOptions": { "types": ["vitest/globals"] } }ES 模块问题
// 确保 package.json 中设置了 type: "module" { "type": "module" }
总结 #
通过以上配置,我们成功为 monorepo 项目中的工具函数库配置了 Vitest 3.x 单元测试。这个配置提供了:
- ⚡ 极速测试执行:基于 Vite 的快速 HMR 和按需编译
- 📊 完整覆盖率报告:支持 v8 和 istanbul 两种覆盖率提供者
- 🔍 精准测试过滤:支持按文件名、行号精确定位测试
- 🌐 浏览器模式:支持在真实浏览器环境中运行测试
- 🎯 多项目支持:通过
projects和workspace配置管理 monorepo - 🛠️ 现代化 API:Vitest 3.x 新增断言和改进的 mock 系统
- ✅ TypeScript 原生支持:无需额外配置即可测试 TS 代码
Vitest 3.x 关键特性 #
| 特性 | 说明 | 版本 |
|---|---|---|
| 按行号过滤 | vitest foo.test.ts:10 | 3.0+ |
| 新断言匹配器 | toBeOneOf, toSatisfy, toHaveBeenCalledExactlyOnceWith | 3.0+ |
| 浏览器模式 | 支持 Playwright/WebdriverIO | 3.0+ |
vi.hoisted() | 解决 mock 提升问题 | 3.0+ |
workspace 配置 | 简化 monorepo 设置 | 3.0+ |
fakeTimers 默认行为 | 现在 mock 所有计时器 API(包括 performance.now()) | 3.0+ |
参考资源 #
这样的测试配置确保了代码质量,提高了开发效率,并为持续集成提供了可靠的基础。