为什么选择 Vitest
Vitest 是 Vite 生态的原生测试框架,由 Anthony Fu(antfu)主导开发。它与 Vite 共享配置和转换管线(transform pipeline),具备 极快的 HMR 级 watch 模式和 开箱即用的 ESM/TypeScript 支持。2024 年后,Vitest 已成为 React/Vue/Svelte 等现代前端项目的首选测试框架。
一、Vitest 核心架构
1.1 运行原理
源文件 → Vite Transform Pipeline → 执行环境(Node / jsdom / happy-dom)→ 测试结果
|
共享 vite.config.ts 的 resolve/alias/plugins
关键优势:
- 无需额外 Babel/SWC 配置:直接复用 Vite 的 esbuild/SWC 转换
- 原生 ESM:无需 CJS 转换,
import/export直接运行 - Watch 模式极快:基于 Vite 的模块依赖图,只重新运行受影响的测试
1.2 核心配置
// vitest.config.ts(或 vite.config.ts 中嵌入)
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
// 执行环境
environment: 'jsdom', // 或 'happy-dom'(更快但兼容性稍弱)
// 全局 API(省去每个文件 import)
globals: true,
// Setup 文件
setupFiles: ['./src/test/setup.ts'],
// 覆盖率
coverage: {
provider: 'istanbul', // 或 'v8'
reporter: ['text', 'html', 'lcov'],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
// 包含/排除
include: ['src/**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', 'e2e'],
},
});
1.3 环境选择指南
| 环境 | 适用场景 | 特点 |
|---|---|---|
node |
纯逻辑、工具函数、Node API | 最快,无 DOM |
jsdom |
组件测试、DOM 操作 | 完整 DOM 模拟,兼容性最好 |
happy-dom |
组件测试(追求速度) | 比 jsdom 快 2-3x,但部分 API 缺失 |
edge-runtime |
Edge Function 测试 | 模拟边缘运行时 |
二、测试编写基础
2.1 基本结构 — describe / it / expect
import { describe, it, expect } from 'vitest';
import { add, divide } from './math';
describe('math utilities', () => {
describe('add', () => {
it('should add two positive numbers', () => {
expect(add(1, 2)).toBe(3);
});
it('should handle negative numbers', () => {
expect(add(-1, -2)).toBe(-3);
});
});
describe('divide', () => {
it('should divide correctly', () => {
expect(divide(10, 2)).toBe(5);
});
it('should throw on division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});
});
2.2 常用 Matcher(断言方法)
// 相等性
expect(value).toBe(primitive); // Object.is 严格相等
expect(value).toEqual(object); // 深度相等(忽略 undefined 属性)
expect(value).toStrictEqual(object); // 深度严格相等
// 真值性
expect(value).toBeTruthy();
expect(value).toBeFalsy();
expect(value).toBeNull();
expect(value).toBeUndefined();
expect(value).toBeDefined();
// 数字
expect(value).toBeGreaterThan(3);
expect(value).toBeCloseTo(0.3, 5); // 浮点数比较
// 字符串
expect(str).toMatch(/regex/);
expect(str).toContain('substring');
// 数组 / 集合
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
expect(arr).toEqual(expect.arrayContaining([1, 2]));
// 对象
expect(obj).toHaveProperty('key', 'value');
expect(obj).toMatchObject({ partial: 'match' });
// 异常
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('specific message');
expect(() => fn()).toThrowError(CustomError);
2.3 测试生命周期
describe('database operations', () => {
let db: Database;
beforeAll(async () => {
// 整个 describe 块开始前执行一次
db = await createTestDatabase();
});
afterAll(async () => {
// 整个 describe 块结束后执行一次
await db.close();
});
beforeEach(async () => {
// 每个 test 前执行
await db.clear();
await db.seed(testData);
});
afterEach(() => {
// 每个 test 后执行(清理)
vi.restoreAllMocks();
});
it('should insert record', async () => {
await db.insert({ name: 'test' });
expect(await db.count()).toBe(testData.length + 1);
});
});
三、Mock 机制深入
3.1 vi.fn() — 创建 Mock 函数
import { vi, describe, it, expect } from 'vitest';
const mockCallback = vi.fn();
// 设置返回值
mockCallback.mockReturnValue(42);
mockCallback.mockReturnValueOnce(1).mockReturnValueOnce(2);
mockCallback.mockResolvedValue({ data: 'async' });
mockCallback.mockImplementation((x: number) => x * 2);
// 断言调用情况
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback).toHaveBeenCalledTimes(3);
expect(mockCallback).toHaveBeenCalledWith('arg1', 'arg2');
expect(mockCallback).toHaveBeenLastCalledWith('latest');
expect(mockCallback).toHaveReturnedWith(42);
3.2 vi.spyOn() — 监听已有方法
import * as mathModule from './math';
const spy = vi.spyOn(mathModule, 'add');
// 保留原始实现,但可以监听调用
mathModule.add(1, 2); // 真正执行
expect(spy).toHaveBeenCalledWith(1, 2);
// 也可以替换实现
spy.mockImplementation(() => 999);
expect(mathModule.add(1, 2)).toBe(999);
// 恢复
spy.mockRestore();
3.3 vi.mock() — 模块级 Mock
// Mock 整个模块
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Test' }),
fetchPosts: vi.fn().mockResolvedValue([]),
}));
// 部分 Mock(保留真实实现)
vi.mock('./utils', async (importOriginal) => {
const actual = await importOriginal<typeof import('./utils')>();
return {
...actual,
// 只 Mock 特定导出
formatDate: vi.fn().mockReturnValue('2025-01-01'),
};
});
Hoisting 机制:vi.mock() 调用会被 Vitest 自动提升(hoist)到文件顶部,这意味着它会在所有 import 之前执行。这是 Vitest/Jest 的核心 Mock 机制。
3.4 手动 Mock(mocks 目录)
src/
utils/
__mocks__/
api.ts ← 手动 Mock 文件
api.ts ← 真实模块
api.test.ts
// api.test.ts
vi.mock('./api'); // 自动使用 __mocks__/api.ts
四、异步测试
4.1 Promise / async-await
it('should fetch user data', async () => {
const user = await fetchUser(1);
expect(user.name).toBe('Alice');
});
// 或使用 resolves/rejects matcher
it('should resolve with user', () => {
return expect(fetchUser(1)).resolves.toEqual({ id: 1, name: 'Alice' });
});
it('should reject for invalid id', () => {
return expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});
4.2 定时器(Fake Timers)
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { debounce } from './debounce';
describe('debounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('should delay execution', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(299);
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(fn).toHaveBeenCalledTimes(1);
});
it('should reset timer on repeated calls', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
vi.advanceTimersByTime(200);
debounced(); // 重置计时器
vi.advanceTimersByTime(200);
expect(fn).not.toHaveBeenCalled(); // 还没到 300ms
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledTimes(1);
});
});
4.3 日期 Mock
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-06-15T10:00:00Z'));
expect(new Date().toISOString()).toBe('2025-06-15T10:00:00.000Z');
expect(Date.now()).toBe(new Date('2025-06-15T10:00:00Z').getTime());
vi.useRealTimers();
五、快照测试(Snapshot Testing)
5.1 Inline Snapshot
it('should format config correctly', () => {
const config = generateConfig({ mode: 'production' });
expect(config).toMatchInlineSnapshot(`
{
"mode": "production",
"minify": true,
"sourcemap": false,
}
`);
});
5.2 File Snapshot
it('should generate correct HTML', () => {
const html = renderToString(<Header title="Hello" />);
expect(html).toMatchSnapshot(); // 存储在 __snapshots__/xxx.test.ts.snap
});
5.3 快照测试的最佳实践
- 小而精确:避免对整个页面做快照,聚焦于特定输出
- 有意义的更新:
vitest --update更新快照前,仔细 review 变化 - 搭配断言使用:快照不替代精确断言,而是补充
六、并行与隔离
6.1 测试并行执行
Vitest 默认文件级并行执行:
// vitest.config.ts
export default defineConfig({
test: {
// 文件级并行(默认开启)
fileParallelism: true,
// 单文件内的测试是否并行
sequence: {
concurrent: false, // 默认关闭,需要时在 describe 级别开启
},
// Worker 池配置
pool: 'forks', // 'threads' | 'forks' | 'vmThreads'
poolOptions: {
forks: {
maxForks: 4,
},
},
},
});
6.2 测试隔离
// 确保每个测试文件有独立的模块注册表
export default defineConfig({
test: {
isolate: true, // 默认 true — 每个文件独立的模块环境
},
});
权衡:isolate: true 更安全但更慢。对于确认没有全局状态污染的项目,可以设为 false 提升速度。
七、从 Jest 迁移
7.1 API 兼容性
Vitest 的 API 高度兼容 Jest,大部分测试代码无需修改:
| Jest | Vitest | 差异 |
|---|---|---|
jest.fn() |
vi.fn() |
命名空间不同 |
jest.mock() |
vi.mock() |
语义相同 |
jest.spyOn() |
vi.spyOn() |
语义相同 |
jest.useFakeTimers() |
vi.useFakeTimers() |
语义相同 |
@jest/globals |
vitest |
导入源不同 |
7.2 迁移步骤
# 1. 安装
npm install -D vitest @vitest/coverage-istanbul
# 2. 配置(复用 vite.config.ts)
# 3. 全局替换
# jest.fn → vi.fn / jest.mock → vi.mock / jest.spyOn → vi.spyOn
# 4. 更新 scripts
# "test": "jest" → "test": "vitest"
# 5. 移除 Jest 相关
npm uninstall jest ts-jest babel-jest @types/jest
7.3 关键差异
- ESM 优先:Vitest 原生支持 ESM,无需
ts-jest或babel-jest - Hoisting 差异:
vi.mock的 hoisting 行为与jest.mock略有不同 - 配置统一:无需
jest.config.js,直接在vite.config.ts中配置
八、高级特性
8.1 Type Testing
import { expectTypeOf, describe, it } from 'vitest';
describe('type tests', () => {
it('should infer correct return type', () => {
expectTypeOf(add).toBeFunction();
expectTypeOf(add).parameter(0).toBeNumber();
expectTypeOf(add(1, 2)).toBeNumber();
});
it('should match complex types', () => {
type User = { id: number; name: string };
expectTypeOf<User>().toMatchTypeOf<{ id: number }>();
});
});
8.2 Benchmark
import { bench, describe } from 'vitest';
describe('array sorting', () => {
const data = Array.from({ length: 10000 }, () => Math.random());
bench('Array.sort', () => {
[...data].sort((a, b) => a - b);
});
bench('custom quicksort', () => {
quicksort([...data]);
});
});
8.3 Browser Mode(实验性)
// vitest.config.ts
export default defineConfig({
test: {
browser: {
enabled: true,
provider: 'playwright',
name: 'chromium',
},
},
});
九、面试高频题
题型 1:vi.fn / vi.spyOn / vi.mock 的区别
vi.fn():创建独立的 Mock 函数,不关联任何模块vi.spyOn():监听已有对象的方法,可选择替换实现或保留原始vi.mock():Mock 整个模块的导出,所有导入该模块的地方都受影响
题型 2:如何测试一个调用了 fetch 的函数?
// 方案 1:vi.mock + vi.fn
vi.mock('./api', () => ({
fetchData: vi.fn().mockResolvedValue({ result: 'mocked' }),
}));
// 方案 2:MSW(推荐,更接近真实)
// 见 mocking-strategies 章节
// 方案 3:vi.spyOn(globalThis, 'fetch')
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response(JSON.stringify({ result: 'mocked' }))
);
题型 3:如何保证测试之间互不影响?
afterEach(() => vi.restoreAllMocks())— 恢复所有 MockafterEach(() => vi.clearAllTimers())— 清理定时器beforeEach中重新初始化测试数据- 避免修改模块级别的全局变量
- 开启
isolate: true(默认)确保模块隔离
十、与其他主题的关联
| 关联主题 | 关系 |
|---|---|
| testing-philosophy | 单元测试是 Testing Pyramid 的底层 |
| component-testing | 组件测试基于 Vitest + Testing Library |
| mocking-strategies | Mock 是单元测试的核心技术 |
| test-coverage | 覆盖率通过 Vitest 的 coverage 插件收集 |
| typescript | Vitest 原生支持 TypeScript,无需额外配置 |
参考资料
- Vitest 官方文档 — vitest.dev
- Vitest GitHub — github.com/vitest-dev/vitest
- Anthony Fu, Why Vitest — Vitest 设计理念
- Jest 官方文档 — jestjs.io — 迁移参考
- Vitest Migration Guide — vitest.dev/guide/migration