vitest
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVitest - Modern TypeScript Testing
Vitest - 现代化TypeScript测试
Overview
概述
Vitest is a next-generation test framework powered by Vite, designed for modern TypeScript/JavaScript projects. It provides blazing-fast test execution through HMR-based test running, native ESM support, and first-class TypeScript integration.
Key Features:
- ⚡ Vite-native: Instant HMR-based test execution (10-100x faster than Jest)
- 🎯 TypeScript-first: Built-in TypeScript support, no configuration needed
- 🔄 ESM-native: Native ES modules, async/await, top-level await
- 🧪 Jest-compatible: Compatible API for easy migration
- 📸 Snapshot testing: Built-in snapshot support
- 🎨 Component testing: React Testing Library, Vue Test Utils integration
- 📊 Coverage: Built-in v8/c8 coverage (faster than Istanbul)
- 🌐 UI mode: Beautiful web UI for test debugging
Installation:
bash
npm install -D vitestVitest是由Vite驱动的下一代测试框架,专为现代化TypeScript/JavaScript项目设计。它通过基于HMR的测试运行机制、原生ESM支持和一流的TypeScript集成,提供极快的测试执行速度。
核心特性:
- ⚡ Vite原生: 基于HMR的即时测试执行(比Jest快10-100倍)
- 🎯 TypeScript优先: 内置TypeScript支持,无需额外配置
- 🔄 ESM原生: 原生ES模块、async/await、顶级await支持
- 🧪 Jest兼容: 兼容Jest的API,便于迁移
- 📸 快照测试: 内置快照测试支持
- 🎨 组件测试: 集成React Testing Library、Vue Test Utils
- 📊 覆盖率统计: 内置v8/c8覆盖率统计(比Istanbul更快)
- 🌐 UI模式: 用于测试调试的美观Web界面
安装:
bash
npm install -D vitestTypeScript types (usually auto-detected)
TypeScript类型(通常会自动检测)
npm install -D @vitest/ui # Optional: UI mode
undefinednpm install -D @vitest/ui # 可选:UI模式
undefinedBasic Setup
基础配置
1. Configure Vitest
1. 配置Vitest
vitest.config.ts:
typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // Use describe/it/expect globally
environment: 'node', // or 'jsdom' for DOM testing
coverage: {
provider: 'v8', // or 'istanbul'
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.test.ts',
'**/*.spec.ts',
],
},
include: ['**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
},
});vitest.config.ts:
typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // 全局使用describe/it/expect
environment: 'node', // 或使用'jsdom'进行DOM测试
coverage: {
provider: 'v8', // 或'istanbul'
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'dist/',
'**/*.test.ts',
'**/*.spec.ts',
],
},
include: ['**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
},
});2. TypeScript Configuration
2. TypeScript配置
tsconfig.json:
json
{
"compilerOptions": {
"types": ["vitest/globals"] // For global describe/it/expect
}
}Alternative (without globals):
typescript
import { describe, it, expect } from 'vitest';tsconfig.json:
json
{
"compilerOptions": {
"types": ["vitest/globals"] // 支持全局的describe/it/expect
}
}替代方案(不使用全局API):
typescript
import { describe, it, expect } from 'vitest';3. Package.json Scripts
3. Package.json脚本
json
{
"scripts": {
"test": "vitest run", // CI mode (single run)
"test:watch": "vitest", // Watch mode (default)
"test:ui": "vitest --ui", // UI mode
"test:coverage": "vitest run --coverage"
}
}json
{
"scripts": {
"test": "vitest run", // CI模式(单次运行)
"test:watch": "vitest", // 监听模式(默认)
"test:ui": "vitest --ui", // UI模式
"test:coverage": "vitest run --coverage"
}
}Core Testing Patterns
核心测试模式
Basic Test Structure
基础测试结构
typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
it('adds two numbers correctly', () => {
const result = calculator.add(2, 3);
expect(result).toBe(5);
});
it('handles negative numbers', () => {
expect(calculator.add(-5, 3)).toBe(-2);
});
});typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('计算器', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
it('正确计算两数之和', () => {
const result = calculator.add(2, 3);
expect(result).toBe(5);
});
it('处理负数相加', () => {
expect(calculator.add(-5, 3)).toBe(-2);
});
});TypeScript Type Testing
TypeScript类型测试
typescript
import { describe, it, expectTypeOf, assertType } from 'vitest';
interface User {
id: number;
name: string;
email: string;
}
describe('Type Safety', () => {
it('ensures correct types', () => {
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
// Type assertions
expectTypeOf(user.id).toBeNumber();
expectTypeOf(user.name).toBeString();
expectTypeOf(user).toMatchTypeOf<User>();
// Assert type at compile time
assertType<User>(user);
});
it('checks function return types', () => {
function getUser(): User {
return { id: 1, name: 'Bob', email: 'bob@example.com' };
}
expectTypeOf(getUser).returns.toMatchTypeOf<User>();
});
});typescript
import { describe, it, expectTypeOf, assertType } from 'vitest';
interface User {
id: number;
name: string;
email: string;
}
describe('类型安全', () => {
it('确保类型正确', () => {
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
};
// 类型断言
expectTypeOf(user.id).toBeNumber();
expectTypeOf(user.name).toBeString();
expectTypeOf(user).toMatchTypeOf<User>();
// 编译时类型断言
assertType<User>(user);
});
it('检查函数返回类型', () => {
function getUser(): User {
return { id: 1, name: 'Bob', email: 'bob@example.com' };
}
expectTypeOf(getUser).returns.toMatchTypeOf<User>();
});
});Mocking and Spies
模拟与间谍
vi.mock for Module Mocking
使用vi.mock模拟模块
typescript
import { describe, it, expect, vi } from 'vitest';
import { fetchUser } from './api';
import { UserService } from './UserService';
// Mock entire module
vi.mock('./api', () => ({
fetchUser: vi.fn(),
}));
describe('UserService', () => {
it('fetches user data', async () => {
const mockUser = { id: 1, name: 'Alice' };
vi.mocked(fetchUser).mockResolvedValue(mockUser);
const service = new UserService();
const user = await service.getUser(1);
expect(fetchUser).toHaveBeenCalledWith(1);
expect(user).toEqual(mockUser);
});
});typescript
import { describe, it, expect, vi } from 'vitest';
import { fetchUser } from './api';
import { UserService } from './UserService';
// 模拟整个模块
vi.mock('./api', () => ({
fetchUser: vi.fn(),
}));
describe('用户服务', () => {
it('获取用户数据', async () => {
const mockUser = { id: 1, name: 'Alice' };
vi.mocked(fetchUser).mockResolvedValue(mockUser);
const service = new UserService();
const user = await service.getUser(1);
expect(fetchUser).toHaveBeenCalledWith(1);
expect(user).toEqual(mockUser);
});
});vi.spyOn for Method Spying
使用vi.spyOn监听方法
typescript
import { describe, it, expect, vi } from 'vitest';
class Logger {
log(message: string) {
console.log(message);
}
}
describe('Logger Spy', () => {
it('tracks method calls', () => {
const logger = new Logger();
const spy = vi.spyOn(logger, 'log');
logger.log('Hello');
logger.log('World');
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith('Hello');
expect(spy).toHaveBeenLastCalledWith('World');
spy.mockRestore(); // Restore original implementation
});
});typescript
import { describe, it, expect, vi } from 'vitest';
class Logger {
log(message: string) {
console.log(message);
}
}
describe('日志监听', () => {
it('跟踪方法调用', () => {
const logger = new Logger();
const spy = vi.spyOn(logger, 'log');
logger.log('Hello');
logger.log('World');
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenCalledWith('Hello');
expect(spy).toHaveBeenLastCalledWith('World');
spy.mockRestore(); // 恢复原始实现
});
});Mock Implementation
自定义模拟实现
typescript
import { describe, it, expect, vi } from 'vitest';
describe('Mock Implementation', () => {
it('provides custom mock implementation', () => {
const mockFn = vi.fn((x: number) => x * 2);
expect(mockFn(5)).toBe(10);
expect(mockFn).toHaveBeenCalledWith(5);
// Change implementation
mockFn.mockImplementation((x: number) => x + 10);
expect(mockFn(5)).toBe(15);
// One-time implementation
mockFn.mockImplementationOnce((x: number) => 100);
expect(mockFn(5)).toBe(100);
expect(mockFn(5)).toBe(15); // Back to default
});
});typescript
import { describe, it, expect, vi } from 'vitest';
describe('自定义模拟实现', () => {
it('提供自定义模拟逻辑', () => {
const mockFn = vi.fn((x: number) => x * 2);
expect(mockFn(5)).toBe(10);
expect(mockFn).toHaveBeenCalledWith(5);
// 更改实现
mockFn.mockImplementation((x: number) => x + 10);
expect(mockFn(5)).toBe(15);
// 单次临时实现
mockFn.mockImplementationOnce((x: number) => 100);
expect(mockFn(5)).toBe(100);
expect(mockFn(5)).toBe(15); // 恢复为默认实现
});
});Mocking Timers
模拟定时器
typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
describe('Timer Mocking', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('fast-forwards time', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
vi.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);
});
it('runs all timers', async () => {
const callback = vi.fn();
setTimeout(callback, 1000);
setTimeout(callback, 2000);
await vi.runAllTimersAsync();
expect(callback).toHaveBeenCalledTimes(2);
});
});typescript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
describe('定时器模拟', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('快进时间', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
vi.advanceTimersByTime(500);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(500);
expect(callback).toHaveBeenCalledTimes(1);
});
it('运行所有定时器', async () => {
const callback = vi.fn();
setTimeout(callback, 1000);
setTimeout(callback, 2000);
await vi.runAllTimersAsync();
expect(callback).toHaveBeenCalledTimes(2);
});
});React Testing Integration
React测试集成
Setup React Testing Library
配置React Testing Library
bash
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D jsdom # For DOM environmentvitest.config.ts (React):
typescript
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
},
});src/test/setup.ts:
typescript
import '@testing-library/jest-dom';
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);
afterEach(() => {
cleanup();
});bash
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D jsdom # 用于DOM环境vitest.config.ts(React项目):
typescript
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.ts',
},
});src/test/setup.ts:
typescript
import '@testing-library/jest-dom';
import { expect, afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';
import * as matchers from '@testing-library/jest-dom/matchers';
expect.extend(matchers);
afterEach(() => {
cleanup();
});React Component Testing
React组件测试
typescript
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('Counter Component', () => {
it('renders initial count', () => {
render(<Counter initialCount={0} />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
it('increments counter on button click', async () => {
const user = userEvent.setup();
render(<Counter initialCount={0} />);
const button = screen.getByRole('button', { name: /increment/i });
await user.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('calls onChange callback', async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(<Counter initialCount={0} onChange={onChange} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(onChange).toHaveBeenCalledWith(1);
});
});typescript
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
describe('计数器组件', () => {
it('渲染初始计数', () => {
render(<Counter initialCount={0} />);
expect(screen.getByText('Count: 0')).toBeInTheDocument();
});
it('点击按钮增加计数', async () => {
const user = userEvent.setup();
render(<Counter initialCount={0} />);
const button = screen.getByRole('button', { name: /increment/i });
await user.click(button);
expect(screen.getByText('Count: 1')).toBeInTheDocument();
});
it('调用onChange回调', async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(<Counter initialCount={0} onChange={onChange} />);
await user.click(screen.getByRole('button', { name: /increment/i }));
expect(onChange).toHaveBeenCalledWith(1);
});
});Testing Hooks
测试Hooks
typescript
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter Hook', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
});
it('increments counter', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('resets counter', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});typescript
import { describe, it, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter Hook', () => {
it('使用默认值初始化', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
});
it('增加计数', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('重置计数', () => {
const { result } = renderHook(() => useCounter(10));
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(10);
});
});Vue Testing Integration
Vue测试集成
Setup Vue Test Utils
配置Vue Test Utils
bash
npm install -D @vue/test-utils @vitejs/plugin-vue
npm install -D happy-dom # Faster alternative to jsdomvitest.config.ts (Vue):
typescript
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'happy-dom',
setupFiles: './src/test/setup.ts',
},
});bash
npm install -D @vue/test-utils @vitejs/plugin-vue
npm install -D happy-dom # 比jsdom更快的替代方案vitest.config.ts(Vue项目):
typescript
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'happy-dom',
setupFiles: './src/test/setup.ts',
},
});Vue Component Testing
Vue组件测试
typescript
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
describe('Counter.vue', () => {
it('renders initial count', () => {
const wrapper = mount(Counter, {
props: { initialCount: 5 },
});
expect(wrapper.text()).toContain('Count: 5');
});
it('increments on button click', async () => {
const wrapper = mount(Counter, {
props: { initialCount: 0 },
});
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('Count: 1');
});
it('emits update event', async () => {
const wrapper = mount(Counter, {
props: { initialCount: 0 },
});
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('update')).toBeTruthy();
expect(wrapper.emitted('update')?.[0]).toEqual([1]);
});
});typescript
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
describe('Counter.vue', () => {
it('渲染初始计数', () => {
const wrapper = mount(Counter, {
props: { initialCount: 5 },
});
expect(wrapper.text()).toContain('Count: 5');
});
it('点击按钮增加计数', async () => {
const wrapper = mount(Counter, {
props: { initialCount: 0 },
});
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('Count: 1');
});
it('触发update事件', async () => {
const wrapper = mount(Counter, {
props: { initialCount: 0 },
});
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('update')).toBeTruthy();
expect(wrapper.emitted('update')?.[0]).toEqual([1]);
});
});Async Testing
异步测试
Testing Promises
测试Promise
typescript
import { describe, it, expect } from 'vitest';
describe('Async Operations', () => {
it('resolves promises', async () => {
const result = await Promise.resolve(42);
expect(result).toBe(42);
});
it('rejects promises', async () => {
await expect(Promise.reject(new Error('Failed'))).rejects.toThrow('Failed');
});
it('uses resolves matcher', async () => {
await expect(Promise.resolve(42)).resolves.toBe(42);
});
});typescript
import { describe, it, expect } from 'vitest';
describe('异步操作', () => {
it('解析Promise', async () => {
const result = await Promise.resolve(42);
expect(result).toBe(42);
});
it('捕获Promise拒绝', async () => {
await expect(Promise.reject(new Error('失败'))).rejects.toThrow('失败');
});
it('使用resolves匹配器', async () => {
await expect(Promise.resolve(42)).resolves.toBe(42);
});
});Testing Async Functions
测试异步函数
typescript
import { describe, it, expect, vi } from 'vitest';
async function fetchData(id: number): Promise<string> {
const response = await fetch(`/api/data/${id}`);
return response.json();
}
describe('Async Functions', () => {
it('fetches data successfully', async () => {
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve('data'),
} as Response)
);
const data = await fetchData(1);
expect(data).toBe('data');
expect(fetch).toHaveBeenCalledWith('/api/data/1');
});
it('handles fetch errors', async () => {
global.fetch = vi.fn(() => Promise.reject(new Error('Network error')));
await expect(fetchData(1)).rejects.toThrow('Network error');
});
});typescript
import { describe, it, expect, vi } from 'vitest';
async function fetchData(id: number): Promise<string> {
const response = await fetch(`/api/data/${id}`);
return response.json();
}
describe('异步函数', () => {
it('成功获取数据', async () => {
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve('data'),
} as Response)
);
const data = await fetchData(1);
expect(data).toBe('data');
expect(fetch).toHaveBeenCalledWith('/api/data/1');
});
it('处理获取错误', async () => {
global.fetch = vi.fn(() => Promise.reject(new Error('网络错误')));
await expect(fetchData(1)).rejects.toThrow('网络错误');
});
});Snapshot Testing
快照测试
Basic Snapshots
基础快照
typescript
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';
describe('UserCard Snapshots', () => {
it('matches snapshot', () => {
const { container } = render(
<UserCard name="Alice" email="alice@example.com" />
);
expect(container.firstChild).toMatchSnapshot();
});
it('matches inline snapshot', () => {
const user = { id: 1, name: 'Bob' };
expect(user).toMatchInlineSnapshot(`
{
"id": 1,
"name": "Bob",
}
`);
});
});typescript
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';
describe('用户卡片快照', () => {
it('匹配快照', () => {
const { container } = render(
<UserCard name="Alice" email="alice@example.com" />
);
expect(container.firstChild).toMatchSnapshot();
});
it('匹配内联快照', () => {
const user = { id: 1, name: 'Bob' };
expect(user).toMatchInlineSnapshot(`
{
"id": 1,
"name": "Bob",
}
`);
});
});Snapshot Serializers
快照序列化器
typescript
import { describe, it, expect } from 'vitest';
expect.addSnapshotSerializer({
test: (val) => val && typeof val.toISOString === 'function',
print: (val) => `Date(${(val as Date).toISOString()})`,
});
describe('Custom Serializers', () => {
it('serializes dates consistently', () => {
const data = {
timestamp: new Date('2024-01-01T00:00:00.000Z'),
user: 'Alice',
};
expect(data).toMatchSnapshot();
});
});typescript
import { describe, it, expect } from 'vitest';
expect.addSnapshotSerializer({
test: (val) => val && typeof val.toISOString === 'function',
print: (val) => `Date(${(val as Date).toISOString()})`,
});
describe('自定义序列化器', () => {
it('一致序列化日期', () => {
const data = {
timestamp: new Date('2024-01-01T00:00:00.000Z'),
user: 'Alice',
};
expect(data).toMatchSnapshot();
});
});Coverage Configuration
覆盖率配置
Advanced Coverage Setup
高级覆盖率设置
vitest.config.ts:
typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
reportsDirectory: './coverage',
exclude: [
'node_modules/',
'dist/',
'**/*.test.ts',
'**/*.spec.ts',
'**/*.config.ts',
'**/types/',
],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
all: true, // Include untested files in coverage report
},
},
});vitest.config.ts:
typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
reportsDirectory: './coverage',
exclude: [
'node_modules/',
'dist/',
'**/*.test.ts',
'**/*.spec.ts',
'**/*.config.ts',
'**/types/',
],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
all: true, // 在覆盖率报告中包含未测试文件
},
},
});Running Coverage
生成覆盖率报告
bash
undefinedbash
undefinedGenerate coverage
生成覆盖率报告
npx vitest run --coverage
npx vitest run --coverage
Coverage with UI
带UI的覆盖率报告
npx vitest --coverage --ui
npx vitest --coverage --ui
Specific threshold enforcement
强制执行特定阈值
npx vitest run --coverage --coverage.lines=90
undefinednpx vitest run --coverage --coverage.lines=90
undefinedMigration from Jest
从Jest迁移
API Compatibility
API兼容性
Vitest provides Jest-compatible API:
typescript
// Jest syntax works in Vitest
import { describe, it, expect, jest } from 'vitest';
// Note: Use 'vi' instead of 'jest' for new code
import { describe, it, expect, vi } from 'vitest';
// Both work, but vi is preferred
const mockFn = vi.fn(); // Preferred
const mockFn2 = jest.fn(); // Also worksVitest提供与Jest兼容的API:
typescript
// Jest语法在Vitest中可用
import { describe, it, expect, jest } from 'vitest';
// 注意:新代码建议使用'vi'替代'jest'
import { describe, it, expect, vi } from 'vitest';
// 两种方式都可行,但vi是推荐方式
const mockFn = vi.fn(); // 推荐
const mockFn2 = jest.fn(); // 也可使用Migration Checklist
迁移检查清单
1. Update Dependencies:
bash
npm uninstall jest @types/jest ts-jest
npm install -D vitest @vitest/ui2. Update package.json:
json
{
"scripts": {
"test": "vitest run", // Was: jest
"test:watch": "vitest" // Was: jest --watch
}
}3. Replace jest.config.js with vitest.config.ts:
typescript
// Old: jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
// New: vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});4. Update Test Files:
typescript
// Change imports
- import { jest } from '@jest/globals';
+ import { vi } from 'vitest';
// Update mocks
- jest.fn()
+ vi.fn()
- jest.spyOn()
+ vi.spyOn()
- jest.mock()
+ vi.mock()1. 更新依赖:
bash
npm uninstall jest @types/jest ts-jest
npm install -D vitest @vitest/ui2. 更新package.json:
json
{
"scripts": {
"test": "vitest run", // 原命令:jest
"test:watch": "vitest" // 原命令:jest --watch
}
}3. 用vitest.config.ts替换jest.config.js:
typescript
// 旧配置:jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
// 新配置:vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});4. 更新测试文件:
typescript
// 更换导入语句
- import { jest } from '@jest/globals';
+ import { vi } from 'vitest';
// 更新模拟语法
- jest.fn()
+ vi.fn()
- jest.spyOn()
+ vi.spyOn()
- jest.mock()
+ vi.mock()Advanced Patterns
高级模式
Concurrent Testing
并发测试
typescript
import { describe, it, expect } from 'vitest';
describe.concurrent('Parallel Tests', () => {
it('test 1', async () => {
await slowOperation();
expect(true).toBe(true);
});
it('test 2', async () => {
await slowOperation();
expect(true).toBe(true);
});
// Both tests run in parallel
});typescript
import { describe, it, expect } from 'vitest';
describe.concurrent('并行测试', () => {
it('测试1', async () => {
await slowOperation();
expect(true).toBe(true);
});
it('测试2', async () => {
await slowOperation();
expect(true).toBe(true);
});
// 两个测试并行运行
});Test Context
测试上下文
typescript
import { describe, it, expect, beforeEach } from 'vitest';
interface TestContext {
user: { id: number; name: string };
api: ApiClient;
}
describe<TestContext>('With Context', () => {
beforeEach((context) => {
context.user = { id: 1, name: 'Alice' };
context.api = new ApiClient();
});
it<TestContext>('uses context', ({ user, api }) => {
expect(user.name).toBe('Alice');
expect(api).toBeDefined();
});
});typescript
import { describe, it, expect, beforeEach } from 'vitest';
interface TestContext {
user: { id: number; name: string };
api: ApiClient;
}
describe<TestContext>('带上下文的测试', () => {
beforeEach((context) => {
context.user = { id: 1, name: 'Alice' };
context.api = new ApiClient();
});
it<TestContext>('使用上下文', ({ user, api }) => {
expect(user.name).toBe('Alice');
expect(api).toBeDefined();
});
});Custom Matchers
自定义匹配器
typescript
import { expect } from 'vitest';
expect.extend({
toBeWithinRange(received: number, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
pass
? `expected ${received} not to be within range ${floor} - ${ceiling}`
: `expected ${received} to be within range ${floor} - ${ceiling}`,
};
},
});
// Usage
expect(100).toBeWithinRange(90, 110);typescript
import { expect } from 'vitest';
expect.extend({
toBeWithinRange(received: number, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
pass
? `期望${received}不在${floor} - ${ceiling}范围内`
: `期望${received}在${floor} - ${ceiling}范围内`,
};
},
});
// 使用示例
expect(100).toBeWithinRange(90, 110);Best Practices
最佳实践
- Use globals: true - Simpler imports, Jest-compatible
- Prefer vi over jest - Use Vitest-native API for new code
- Use v8 coverage - Faster than Istanbul, works with native ESM
- Test in isolation - Each test should be independent
- Mock external dependencies - Network, file system, timers
- Use TypeScript - Full type safety in tests
- Run tests in CI mode - Use for CI, not watch mode
vitest run - Leverage UI mode - Debug failing tests visually
- Use describe.concurrent - Parallelize independent tests
- Keep tests focused - One assertion per test when possible
- 启用globals: true - 简化导入,兼容Jest
- 优先使用vi而非jest - 新代码使用Vitest原生API
- 使用v8覆盖率 - 比Istanbul更快,支持原生ESM
- 测试隔离 - 每个测试应独立运行
- 模拟外部依赖 - 网络、文件系统、定时器等
- 使用TypeScript - 测试中实现完全类型安全
- CI环境使用CI模式 - CI中使用而非监听模式
vitest run - 利用UI模式 - 可视化调试失败的测试
- 使用describe.concurrent - 并行执行独立测试
- 保持测试聚焦 - 尽可能每个测试对应一个断言
Common Pitfalls
常见陷阱
❌ Not using CI mode in CI/CD:
json
// WRONG - watch mode hangs in CI
"test": "vitest"
// CORRECT - single run
"test": "vitest run"✅ Correct approach:
json
{
"scripts": {
"test": "vitest run", // CI-safe
"test:watch": "vitest", // Development
"test:ui": "vitest --ui" // Debugging
}
}❌ Forgetting to await async tests:
typescript
// WRONG - test passes before assertion
it('fetches data', () => {
fetchData().then(data => {
expect(data).toBeDefined(); // Never runs!
});
});
// CORRECT
it('fetches data', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});❌ Not cleaning up mocks:
typescript
// WRONG - mocks leak between tests
it('test 1', () => {
vi.spyOn(console, 'log');
// No cleanup!
});
// CORRECT
import { afterEach } from 'vitest';
afterEach(() => {
vi.restoreAllMocks();
});❌ Using wrong environment:
typescript
// WRONG - testing DOM in node environment
test: {
environment: 'node', // Can't test React components!
}
// CORRECT
test: {
environment: 'jsdom', // For React/Vue components
}❌ CI/CD中未使用CI模式:
json
// 错误 - 监听模式在CI中会挂起
"test": "vitest"
// 正确 - 单次运行
"test": "vitest run"✅ 正确做法:
json
{
"scripts": {
"test": "vitest run", // 安全适配CI
"test:watch": "vitest", // 开发环境使用
"test:ui": "vitest --ui" // 调试使用
}
}❌ 异步测试未使用await:
typescript
// 错误 - 测试在断言前就通过了
it('获取数据', () => {
fetchData().then(data => {
expect(data).toBeDefined(); // 永远不会执行!
});
});
// 正确
it('获取数据', async () => {
const data = await fetchData();
expect(data).toBeDefined();
});❌ 未清理模拟:
typescript
// 错误 - 模拟会在测试间泄漏
it('测试1', () => {
vi.spyOn(console, 'log');
// 没有清理!
});
// 正确
import { afterEach } from 'vitest';
afterEach(() => {
vi.restoreAllMocks();
});❌ 使用错误的测试环境:
typescript
// 错误 - 在node环境中测试DOM
test: {
environment: 'node', // 无法测试React组件!
}
// 正确
test: {
environment: 'jsdom', // 用于React/Vue组件测试
}Resources
资源
- Documentation: https://vitest.dev
- API Reference: https://vitest.dev/api/
- Migration Guide: https://vitest.dev/guide/migration.html
- Examples: https://github.com/vitest-dev/vitest/tree/main/examples
- UI Mode: https://vitest.dev/guide/ui.html
Related Skills
相关技能
When using Vitest, consider these complementary skills:
- typescript-core: Advanced TypeScript type patterns, tsconfig, and runtime validation
- react: React component testing with Testing Library integration
- test-driven-development: Complete TDD workflow (RED/GREEN/REFACTOR cycle)
使用Vitest时,可结合以下互补技能:
- typescript-core: 高级TypeScript类型模式、tsconfig配置和运行时验证
- react: 结合Testing Library进行React组件测试
- test-driven-development: 完整的TDD工作流(RED/GREEN/REFACTOR循环)
Quick TypeScript Type Patterns (Inlined for Standalone Use)
快速TypeScript类型模式(独立使用)
typescript
// Type-safe test factories with generics
function createMockData<T extends Record<string, unknown>>(
defaults: T,
overrides?: Partial<T>
): T {
return { ...defaults, ...overrides };
}
const mockUser = createMockData(
{ id: 1, name: 'Test', email: 'test@example.com' },
{ name: 'Alice' }
);
// Runtime validation with Zod in tests
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
test('API returns valid user', async () => {
const response = await fetch('/api/user/1');
const data = await response.json();
// Runtime validation + type inference
const user = UserSchema.parse(data);
expect(user.email).toContain('@');
});
// Const type parameters for literal inference
const createTestConfig = <const T extends Record<string, unknown>>(config: T): T => config;
const testEnv = createTestConfig({ mode: 'test', debug: false });
// Type: { mode: "test"; debug: false } (literals preserved)typescript
// 带泛型的类型安全测试工厂
function createMockData<T extends Record<string, unknown>>(
defaults: T,
overrides?: Partial<T>
): T {
return { ...defaults, ...overrides };
}
const mockUser = createMockData(
{ id: 1, name: 'Test', email: 'test@example.com' },
{ name: 'Alice' }
);
// 测试中使用Zod进行运行时验证
import { z } from 'zod';
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
test('API返回合法用户', async () => {
const response = await fetch('/api/user/1');
const data = await response.json();
// 运行时验证 + 类型推断
const user = UserSchema.parse(data);
expect(user.email).toContain('@');
});
// 常量类型参数保留字面量类型
const createTestConfig = <const T extends Record<string, unknown>>(config: T): T => config;
const testEnv = createTestConfig({ mode: 'test', debug: false });
// 类型: { mode: "test"; debug: false }(保留字面量)Quick React Testing Patterns (Inlined for Standalone Use)
快速React测试模式(独立使用)
typescript
// React Testing Library with Vitest
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { describe, test, expect, vi } from 'vitest';
// Component testing
describe('UserProfile', () => {
test('renders user information', () => {
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
render(<UserProfile user={user} />);
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
test('handles form submission', async () => {
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
const user = userEvent.setup();
await user.type(screen.getByLabelText('Name'), 'Bob');
await user.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ name: 'Bob' });
});
});
});
// Hook testing
import { renderHook, act } from '@testing-library/react';
test('useCounter hook increments', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});typescript
// Vitest结合React Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { describe, test, expect, vi } from 'vitest';
// 组件测试
describe('用户资料', () => {
test('渲染用户信息', () => {
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
render(<UserProfile user={user} />);
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
test('处理表单提交', async () => {
const onSubmit = vi.fn();
render(<UserForm onSubmit={onSubmit} />);
const user = userEvent.setup();
await user.type(screen.getByLabelText('Name'), 'Bob');
await user.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => {
expect(onSubmit).toHaveBeenCalledWith({ name: 'Bob' });
});
});
});
// Hook测试
import { renderHook, act } from '@testing-library/react';
test('useCounter Hook增加计数', () => {
const { result } = renderHook(() => useCounter(0));
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Quick TDD Workflow Reference (Inlined for Standalone Use)
快速TDD工作流参考(独立使用)
RED → GREEN → REFACTOR Cycle:
-
RED Phase: Write Failing Testtypescript
test('should authenticate user with valid credentials', () => { const user = { username: 'alice', password: 'secret123' }; const result = authenticate(user); expect(result.isAuthenticated).toBe(true); // This fails because authenticate() doesn't exist yet }); -
GREEN Phase: Make It Passtypescript
function authenticate(user: User): AuthResult { // Minimum code to pass the test if (user.username === 'alice' && user.password === 'secret123') { return { isAuthenticated: true }; } return { isAuthenticated: false }; } -
REFACTOR Phase: Improve Codetypescript
function authenticate(user: User): AuthResult { // Clean up while keeping tests green const hashed = hashPassword(user.password); const storedUser = database.getUser(user.username); return { isAuthenticated: storedUser?.passwordHash === hashed }; }
Test Structure: Arrange-Act-Assert (AAA)
typescript
test('creates user successfully', async () => {
// Arrange: Set up test data
const userData = { username: 'alice', email: 'alice@example.com' };
// Act: Perform the action
const user = await createUser(userData);
// Assert: Verify outcome
expect(user.username).toBe('alice');
expect(user.email).toBe('alice@example.com');
});Vitest-Specific TDD Features:
typescript
// Watch mode with HMR (instant feedback)
// vitest --watch
// UI mode for visual debugging
// vitest --ui
// Run only changed tests
// vitest --changed
// Benchmark mode for performance testing
import { bench } from 'vitest';
bench('authenticate performance', () => {
authenticate({ username: 'alice', password: 'secret' });
});[Full TypeScript, React, and TDD workflows available in respective skills if deployed together]
RED → GREEN → REFACTOR循环:
-
RED阶段:编写失败的测试typescript
test('使用有效凭证验证用户', () => { const user = { username: 'alice', password: 'secret123' }; const result = authenticate(user); expect(result.isAuthenticated).toBe(true); // 此时authenticate()尚未实现,测试失败 }); -
GREEN阶段:让测试通过typescript
function authenticate(user: User): AuthResult { // 编写最小代码让测试通过 if (user.username === 'alice' && user.password === 'secret123') { return { isAuthenticated: true }; } return { isAuthenticated: false }; } -
REFACTOR阶段:优化代码typescript
function authenticate(user: User): AuthResult { // 在保持测试通过的同时优化代码 const hashed = hashPassword(user.password); const storedUser = database.getUser(user.username); return { isAuthenticated: storedUser?.passwordHash === hashed }; }
测试结构:Arrange-Act-Assert (AAA)
typescript
test('成功创建用户', async () => {
// Arrange:准备测试数据
const userData = { username: 'alice', email: 'alice@example.com' };
// Act:执行操作
const user = await createUser(userData);
// Assert:验证结果
expect(user.username).toBe('alice');
expect(user.email).toBe('alice@example.com');
});Vitest专属TDD特性:
typescript
// 带HMR的监听模式(即时反馈)
// vitest --watch
// 用于可视化调试的UI模式
// vitest --ui
// 仅运行变更的测试
// vitest --changed
// 性能测试基准模式
import { bench } from 'vitest';
bench('验证性能', () => {
authenticate({ username: 'alice', password: 'secret' });
});[完整的TypeScript、React和TDD工作流可在部署相关技能后获取]
Summary
总结
- Vitest is the modern standard for TypeScript testing
- 10-100x faster than Jest through Vite-native HMR
- ESM-first with native module support
- Jest-compatible API for easy migration
- TypeScript-first with built-in type support
- Component testing for React and Vue
- v8 coverage faster than Istanbul
- UI mode for visual test debugging
- Perfect for: Modern TypeScript projects, Vite-based apps, React/Vue components
- Vitest是现代化TypeScript测试的标准方案
- 比Jest快10-100倍,得益于Vite原生HMR
- ESM优先,支持原生模块
- Jest兼容的API便于迁移
- TypeScript优先,内置类型支持
- 支持React和Vue组件测试
- v8覆盖率比Istanbul更快
- UI模式用于可视化测试调试
- 适用场景: 现代化TypeScript项目、基于Vite的应用、React/Vue组件