Vitest

作者 bobmatnyc718070a7d622MIT77 个星标收录于 2026年10月8日更新于 2026年10月8日仓库2个月前更新

Vitest - Modern TypeScript testing framework with Vite-native performance, ESM support, and TypeScript-first design

AI 生成的概览

关于使用 Vitest(Vite 原生的 TypeScript 测试框架)编写和运行测试的参考指南。

功能
提供 Vitest 的配置与使用指导,涵盖 vitest.config.ts 设置、TypeScript 与 ESM 选项以及 package.json 测试脚本。文档介绍核心测试模式,包括断言、类型测试、使用 vi.mock 的模块模拟、spy、假定时器、异步测试、快照和覆盖率阈值。还包含 React 与 Vue 组件测试配置、从 Jest 迁移的清单、最佳实践和常见陷阱。
适用场景
适用于为 TypeScript 或 JavaScript 项目、React 或 Vue 组件以及基于 Vite 的项目编写或配置测试时。也适合将现有 Jest 测试套件迁移到 Vitest,或需要更快测试执行速度的场景。
运行要求
仅为说明文档,不附带脚本。按指南操作需要 Node.js 环境、npm 以及已安装的 vitest 包,还可选装 @vitest/ui、jsdom 或 happy-dom、@testing-library/react、@vue/test-utils 及相应的 Vite 插件。

Vitest - Modern TypeScript Testing

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 vitest# TypeScript types (usually auto-detected)npm install -D @vitest/ui  # Optional: UI mode

Basic Setup

1. Configure 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'],  },});

2. TypeScript Configuration

tsconfig.json:

json
{  "compilerOptions": {    "types": ["vitest/globals"]  // For global describe/it/expect  }}

Alternative (without globals):

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

3. Package.json Scripts

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"  }}

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 Type Testing

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: '[email protected]',    };
    // 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: '[email protected]' };    }
    expectTypeOf(getUser).returns.toMatchTypeOf<User>();  });});

Mocking and Spies

vi.mock for Module Mocking

typescript
import { describe, it, expect, vi } from 'vitest';import { fetchUser } from './api';import { UserService } from './UserService';
// Mock entire modulevi.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);  });});

vi.spyOn for Method Spying

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  });});

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  });});

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);  });});

React Testing Integration

Setup React Testing Library

bash
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-eventnpm install -D jsdom  # For DOM environment

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

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);  });});

Testing 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);  });});

Vue Testing Integration

Setup Vue Test Utils

bash
npm install -D @vue/test-utils @vitejs/plugin-vuenpm install -D happy-dom  # Faster alternative to 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

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]);  });});

Async Testing

Testing Promises

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);  });});

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');  });});

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="[email protected]" />    );
    expect(container.firstChild).toMatchSnapshot();  });
  it('matches inline snapshot', () => {    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();  });});

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    },  },});

Running Coverage

bash
# Generate coveragenpx vitest run --coverage
# Coverage with UInpx vitest --coverage --ui
# Specific threshold enforcementnpx vitest run --coverage --coverage.lines=90

Migration from Jest

API Compatibility

Vitest provides Jest-compatible API:

typescript
// Jest syntax works in Vitestimport { describe, it, expect, jest } from 'vitest';
// Note: Use 'vi' instead of 'jest' for new codeimport { describe, it, expect, vi } from 'vitest';
// Both work, but vi is preferredconst mockFn = vi.fn();  // Preferredconst mockFn2 = jest.fn();  // Also works

Migration Checklist

1. Update Dependencies:

bash
npm uninstall jest @types/jest ts-jestnpm install -D vitest @vitest/ui

2. 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.jsmodule.exports = {  preset: 'ts-jest',  testEnvironment: 'node',};
// New: vitest.config.tsimport { 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()

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});

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();  });});

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}`,    };  },});
// Usageexpect(100).toBeWithinRange(90, 110);

Best Practices

  1. Use globals: true - Simpler imports, Jest-compatible
  2. Prefer vi over jest - Use Vitest-native API for new code
  3. Use v8 coverage - Faster than Istanbul, works with native ESM
  4. Test in isolation - Each test should be independent
  5. Mock external dependencies - Network, file system, timers
  6. Use TypeScript - Full type safety in tests
  7. Run tests in CI mode - Use vitest run for CI, not watch mode
  8. Leverage UI mode - Debug failing tests visually
  9. Use describe.concurrent - Parallelize independent tests
  10. Keep tests focused - One assertion per test when possible

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 assertionit('fetches data', () => {  fetchData().then(data => {    expect(data).toBeDefined();  // Never runs!  });});
// CORRECTit('fetches data', async () => {  const data = await fetchData();  expect(data).toBeDefined();});

❌ Not cleaning up mocks:

typescript
// WRONG - mocks leak between testsit('test 1', () => {  vi.spyOn(console, 'log');  // No cleanup!});
// CORRECTimport { afterEach } from 'vitest';
afterEach(() => {  vi.restoreAllMocks();});

❌ Using wrong environment:

typescript
// WRONG - testing DOM in node environmenttest: {  environment: 'node',  // Can't test React components!}
// CORRECTtest: {  environment: 'jsdom',  // For React/Vue components}

Resources

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)

Quick TypeScript Type Patterns (Inlined for Standalone Use)

typescript
// Type-safe test factories with genericsfunction createMockData<T extends Record<string, unknown>>(  defaults: T,  overrides?: Partial<T>): T {  return { ...defaults, ...overrides };}
const mockUser = createMockData(  { id: 1, name: 'Test', email: '[email protected]' },  { name: 'Alice' });
// Runtime validation with Zod in testsimport { 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 inferenceconst 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)

Quick React Testing Patterns (Inlined for Standalone Use)

typescript
// React Testing Library with Vitestimport { render, screen, fireEvent, waitFor } from '@testing-library/react';import { userEvent } from '@testing-library/user-event';import { describe, test, expect, vi } from 'vitest';
// Component testingdescribe('UserProfile', () => {  test('renders user information', () => {    const user = { id: 1, name: 'Alice', email: '[email protected]' };    render(<UserProfile user={user} />);
    expect(screen.getByText('Alice')).toBeInTheDocument();    expect(screen.getByText('[email protected]')).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 testingimport { 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);});

Quick TDD Workflow Reference (Inlined for Standalone Use)

RED → GREEN → REFACTOR Cycle:

  1. RED Phase: Write Failing Test

    typescript
    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});
  2. GREEN Phase: Make It Pass

    typescript
    function authenticate(user: User): AuthResult {  // Minimum code to pass the test  if (user.username === 'alice' && user.password === 'secret123') {    return { isAuthenticated: true };  }  return { isAuthenticated: false };}
  3. REFACTOR Phase: Improve Code

    typescript
    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: '[email protected]' };
  // Act: Perform the action  const user = await createUser(userData);
  // Assert: Verify outcome  expect(user.username).toBe('alice');  expect(user.email).toBe('[email protected]');});

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 testingimport { bench } from 'vitest';
bench('authenticate performance', () => {  authenticate({ username: 'alice', password: 'secret' });});

[Full TypeScript, React, and TDD workflows available in respective skills if deployed together]

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

来源与署名

来源:bobmatnyc/claude-mpm-skills位于toolchains/typescript/testing/vitest提交718070a

许可证: MIT

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架