jest-typescript

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Jest + TypeScript - Industry Standard Testing

Jest + TypeScript - 行业标准测试方案

Overview

概述

Jest is the industry-standard testing framework with 70% market share, providing a mature, battle-tested ecosystem for TypeScript projects. It offers comprehensive testing capabilities with built-in snapshot testing, mocking, and coverage reporting.
Key Features:
  • 🏆 Industry Standard: 70% market share, widely adopted
  • 📦 All-in-One: Test runner, assertions, mocks, coverage in one package
  • 📸 Snapshot Testing: Built-in snapshot support for UI testing
  • 🧪 React Integration: React Testing Library, enzyme compatibility
  • 🔧 Mature Ecosystem: Extensive plugins, tooling, and community support
  • 🎯 TypeScript Support: Full type safety via ts-jest
  • 🔍 Coverage Reports: Built-in Istanbul coverage
  • 🌐 Multi-Platform: Node.js, browser (jsdom), React Native
Installation:
bash
npm install -D jest @types/jest ts-jest
npm install -D @testing-library/react @testing-library/jest-dom  # For React
Jest是占据70%市场份额的行业标准测试框架,为TypeScript项目提供成熟且经过实战检验的生态系统。它具备全面的测试能力,内置快照测试、模拟和覆盖率报告功能。
核心特性:
  • 🏆 行业标准:占据70%市场份额,被广泛采用
  • 📦 一体化工具:集成测试运行器、断言、模拟、覆盖率功能于单个包中
  • 📸 快照测试:内置UI测试的快照支持
  • 🧪 React集成:兼容React Testing Library与enzyme
  • 🔧 成熟生态:拥有丰富的插件、工具及社区支持
  • 🎯 TypeScript支持:通过ts-jest实现完整的类型安全
  • 🔍 覆盖率报告:内置Istanbul覆盖率统计
  • 🌐 多平台适配:支持Node.js、浏览器(jsdom)、React Native
安装:
bash
npm install -D jest @types/jest ts-jest
npm install -D @testing-library/react @testing-library/jest-dom  # 针对React项目

Basic Setup

基础配置

1. Initialize Jest Configuration

1. 初始化Jest配置

bash
npx ts-jest config:init
This creates jest.config.js:
javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};
bash
npx ts-jest config:init
这会生成jest.config.js文件:
javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

2. Manual Configuration

2. 手动配置

jest.config.ts (TypeScript config):
typescript
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/__tests__/**',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

export default config;
jest.config.ts(TypeScript格式配置):
typescript
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/__tests__/**',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

export default config;

3. TypeScript Configuration

3. TypeScript配置

tsconfig.json:
json
{
  "compilerOptions": {
    "types": ["jest", "@testing-library/jest-dom"],
    "esModuleInterop": true
  }
}
tsconfig.test.json (test-specific):
json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "types": ["jest", "node", "@testing-library/jest-dom"]
  },
  "include": ["src/**/*.test.ts", "src/**/*.spec.ts", "src/**/__tests__/**"]
}
tsconfig.json:
json
{
  "compilerOptions": {
    "types": ["jest", "@testing-library/jest-dom"],
    "esModuleInterop": true
  }
}
tsconfig.test.json(测试专用配置):
json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "types": ["jest", "node", "@testing-library/jest-dom"]
  },
  "include": ["src/**/*.test.ts", "src/**/*.spec.ts", "src/**/__tests__/**"]
}

4. Package.json Scripts

4. Package.json脚本

json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --maxWorkers=2"
  }
}
json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "test:ci": "jest --ci --coverage --maxWorkers=2"
  }
}

Core Testing Patterns

核心测试模式

Basic Test Structure

基础测试结构

typescript
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';

describe('Calculator', () => {
  let calculator: Calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  afterEach(() => {
    // Cleanup
  });

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

  it.each([
    [1, 1, 2],
    [2, 3, 5],
    [10, -5, 5],
  ])('adds %i + %i to equal %i', (a, b, expected) => {
    expect(calculator.add(a, b)).toBe(expected);
  });
});
typescript
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';

describe('Calculator', () => {
  let calculator: Calculator;

  beforeEach(() => {
    calculator = new Calculator();
  });

  afterEach(() => {
    // 清理操作
  });

  it('正确计算两数之和', () => {
    const result = calculator.add(2, 3);
    expect(result).toBe(5);
  });

  it('处理负数相加', () => {
    expect(calculator.add(-5, 3)).toBe(-2);
  });

  it.each([
    [1, 1, 2],
    [2, 3, 5],
    [10, -5, 5],
  ])('%i + %i 等于 %i', (a, b, expected) => {
    expect(calculator.add(a, b)).toBe(expected);
  });
});

TypeScript Type-Safe Tests

TypeScript类型安全测试

typescript
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

describe('User Service', () => {
  it('creates user with correct types', () => {
    const user: User = {
      id: 1,
      name: 'Alice',
      email: 'alice@example.com',
      role: 'admin',
    };

    // Type-safe assertions
    expect(user.id).toEqual(expect.any(Number));
    expect(user.name).toEqual(expect.any(String));
    expect(user.role).toMatch(/^(admin|user)$/);
  });

  it('validates user object shape', () => {
    const user = createUser('Bob', 'bob@example.com');

    expect(user).toMatchObject({
      id: expect.any(Number),
      name: 'Bob',
      email: 'bob@example.com',
    });
  });
});
typescript
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

describe('用户服务', () => {
  it('创建类型正确的用户对象', () => {
    const user: User = {
      id: 1,
      name: 'Alice',
      email: 'alice@example.com',
      role: 'admin',
    };

    // 类型安全断言
    expect(user.id).toEqual(expect.any(Number));
    expect(user.name).toEqual(expect.any(String));
    expect(user.role).toMatch(/^(admin|user)$/);
  });

  it('验证用户对象结构', () => {
    const user = createUser('Bob', 'bob@example.com');

    expect(user).toMatchObject({
      id: expect.any(Number),
      name: 'Bob',
      email: 'bob@example.com',
    });
  });
});

Mocking with TypeScript

使用TypeScript进行模拟

jest.mock for Module Mocking

jest.mock模块模拟

typescript
import { jest } from '@jest/globals';
import { UserService } from './UserService';
import * as userApi from './api/userApi';

// Mock entire module
jest.mock('./api/userApi');

describe('UserService with Mocks', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('fetches user data', async () => {
    const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' };

    // Type-safe mock
    const mockedFetchUser = jest.mocked(userApi.fetchUser);
    mockedFetchUser.mockResolvedValue(mockUser);

    const service = new UserService();
    const user = await service.getUser(1);

    expect(mockedFetchUser).toHaveBeenCalledWith(1);
    expect(user).toEqual(mockUser);
  });
});
typescript
import { jest } from '@jest/globals';
import { UserService } from './UserService';
import * as userApi from './api/userApi';

// 模拟整个模块
jest.mock('./api/userApi');

describe('带模拟的用户服务', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('获取用户数据', async () => {
    const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' };

    // 类型安全模拟
    const mockedFetchUser = jest.mocked(userApi.fetchUser);
    mockedFetchUser.mockResolvedValue(mockUser);

    const service = new UserService();
    const user = await service.getUser(1);

    expect(mockedFetchUser).toHaveBeenCalledWith(1);
    expect(user).toEqual(mockUser);
  });
});

jest.spyOn for Method Spying

jest.spyOn方法监听

typescript
import { jest } from '@jest/globals';

class Logger {
  log(message: string): void {
    console.log(message);
  }

  error(message: string): void {
    console.error(message);
  }
}

describe('Logger Spy', () => {
  let logger: Logger;
  let logSpy: jest.SpyInstance;

  beforeEach(() => {
    logger = new Logger();
    logSpy = jest.spyOn(logger, 'log');
  });

  afterEach(() => {
    logSpy.mockRestore();
  });

  it('tracks method calls', () => {
    logger.log('Hello');
    logger.log('World');

    expect(logSpy).toHaveBeenCalledTimes(2);
    expect(logSpy).toHaveBeenCalledWith('Hello');
    expect(logSpy).toHaveBeenLastCalledWith('World');
  });

  it('provides custom implementation', () => {
    logSpy.mockImplementation((msg: string) => {
      console.log(`[CUSTOM] ${msg}`);
    });

    logger.log('Test');
    expect(logSpy).toHaveBeenCalledWith('Test');
  });
});
typescript
import { jest } from '@jest/globals';

class Logger {
  log(message: string): void {
    console.log(message);
  }

  error(message: string): void {
    console.error(message);
  }
}

describe('日志器监听', () => {
  let logger: Logger;
  let logSpy: jest.SpyInstance;

  beforeEach(() => {
    logger = new Logger();
    logSpy = jest.spyOn(logger, 'log');
  });

  afterEach(() => {
    logSpy.mockRestore();
  });

  it('追踪方法调用次数', () => {
    logger.log('Hello');
    logger.log('World');

    expect(logSpy).toHaveBeenCalledTimes(2);
    expect(logSpy).toHaveBeenCalledWith('Hello');
    expect(logSpy).toHaveBeenLastCalledWith('World');
  });

  it('提供自定义实现', () => {
    logSpy.mockImplementation((msg: string) => {
      console.log(`[自定义] ${msg}`);
    });

    logger.log('Test');
    expect(logSpy).toHaveBeenCalledWith('Test');
  });
});

Type-Safe Mock Functions

类型安全模拟函数

typescript
import { jest } from '@jest/globals';

interface ApiResponse<T> {
  data: T;
  status: number;
}

type FetchUserFn = (id: number) => Promise<ApiResponse<User>>;

describe('Type-Safe Mocks', () => {
  it('creates typed mock function', async () => {
    const mockFetchUser = jest.fn<FetchUserFn>()
      .mockResolvedValue({
        data: { id: 1, name: 'Alice', email: 'alice@example.com', role: 'user' },
        status: 200,
      });

    const result = await mockFetchUser(1);

    expect(result.data.name).toBe('Alice');
    expect(result.status).toBe(200);
    expect(mockFetchUser).toHaveBeenCalledWith(1);
  });

  it('uses mock implementation', () => {
    const mockCalculate = jest.fn<(x: number, y: number) => number>()
      .mockImplementation((x, y) => x + y);

    expect(mockCalculate(5, 3)).toBe(8);
    expect(mockCalculate).toHaveBeenCalledWith(5, 3);
  });
});
typescript
import { jest } from '@jest/globals';

interface ApiResponse<T> {
  data: T;
  status: number;
}

type FetchUserFn = (id: number) => Promise<ApiResponse<User>>;

describe('类型安全模拟', () => {
  it('创建带类型的模拟函数', async () => {
    const mockFetchUser = jest.fn<FetchUserFn>()
      .mockResolvedValue({
        data: { id: 1, name: 'Alice', email: 'alice@example.com', role: 'user' },
        status: 200,
      });

    const result = await mockFetchUser(1);

    expect(result.data.name).toBe('Alice');
    expect(result.status).toBe(200);
    expect(mockFetchUser).toHaveBeenCalledWith(1);
  });

  it('使用模拟实现', () => {
    const mockCalculate = jest.fn<(x: number, y: number) => number>()
      .mockImplementation((x, y) => x + y);

    expect(mockCalculate(5, 3)).toBe(8);
    expect(mockCalculate).toHaveBeenCalledWith(5, 3);
  });
});

Mocking Timers

模拟定时器

typescript
import { jest } from '@jest/globals';

describe('Timer Mocking', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('fast-forwards time', () => {
    const callback = jest.fn();
    setTimeout(callback, 1000);

    jest.advanceTimersByTime(500);
    expect(callback).not.toHaveBeenCalled();

    jest.advanceTimersByTime(500);
    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('runs all timers', () => {
    const callback = jest.fn();
    setTimeout(callback, 1000);
    setTimeout(callback, 2000);

    jest.runAllTimers();
    expect(callback).toHaveBeenCalledTimes(2);
  });

  it('handles intervals', () => {
    const callback = jest.fn();
    setInterval(callback, 1000);

    jest.advanceTimersByTime(3500);
    expect(callback).toHaveBeenCalledTimes(3);

    jest.clearAllTimers();
  });
});
typescript
import { jest } from '@jest/globals';

describe('定时器模拟', () => {
  beforeEach(() => {
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it('快进时间', () => {
    const callback = jest.fn();
    setTimeout(callback, 1000);

    jest.advanceTimersByTime(500);
    expect(callback).not.toHaveBeenCalled();

    jest.advanceTimersByTime(500);
    expect(callback).toHaveBeenCalledTimes(1);
  });

  it('运行所有定时器', () => {
    const callback = jest.fn();
    setTimeout(callback, 1000);
    setTimeout(callback, 2000);

    jest.runAllTimers();
    expect(callback).toHaveBeenCalledTimes(2);
  });

  it('处理间隔定时器', () => {
    const callback = jest.fn();
    setInterval(callback, 1000);

    jest.advanceTimersByTime(3500);
    expect(callback).toHaveBeenCalledTimes(3);

    jest.clearAllTimers();
  });
});

React Testing Library + TypeScript

React Testing Library + TypeScript

Setup for React

React项目配置

bash
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D jest-environment-jsdom
jest.config.ts (React):
typescript
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
  },
  transform: {
    '^.+\\.tsx?$': ['ts-jest', {
      tsconfig: {
        jsx: 'react-jsx',
      },
    }],
  },
};

export default config;
src/test/setup.ts:
typescript
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from '@jest/globals';

afterEach(() => {
  cleanup();
});
bash
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
npm install -D jest-environment-jsdom
jest.config.ts(React专用):
typescript
import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
  },
  transform: {
    '^.+\\.tsx?$': ['ts-jest', {
      tsconfig: {
        jsx: 'react-jsx',
      },
    }],
  },
};

export default config;
src/test/setup.ts:
typescript
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from '@jest/globals';

afterEach(() => {
  cleanup();
});

React Component Testing

React组件测试

typescript
import { render, screen, waitFor } 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 with correct value', async () => {
    const onChange = jest.fn();
    const user = userEvent.setup();

    render(<Counter initialCount={5} onChange={onChange} />);

    await user.click(screen.getByRole('button', { name: /increment/i }));

    expect(onChange).toHaveBeenCalledWith(6);
    expect(onChange).toHaveBeenCalledTimes(1);
  });

  it('disables button when max count reached', () => {
    render(<Counter initialCount={10} maxCount={10} />);

    const button = screen.getByRole('button', { name: /increment/i });
    expect(button).toBeDisabled();
  });
});
typescript
import { render, screen, waitFor } 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 = jest.fn();
    const user = userEvent.setup();

    render(<Counter initialCount={5} onChange={onChange} />);

    await user.click(screen.getByRole('button', { name: /increment/i }));

    expect(onChange).toHaveBeenCalledWith(6);
    expect(onChange).toHaveBeenCalledTimes(1);
  });

  it('达到最大计数时禁用按钮', () => {
    render(<Counter initialCount={10} maxCount={10} />);

    const button = screen.getByRole('button', { name: /increment/i });
    expect(button).toBeDisabled();
  });
});

Testing Hooks

测试自定义Hook

typescript
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('decrements counter', () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(4);
  });

  it('resets to initial value', () => {
    const { result } = renderHook(() => useCounter(10));

    act(() => {
      result.current.increment();
      result.current.increment();
    });

    expect(result.current.count).toBe(12);

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(10);
  });
});
typescript
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(5));

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(4);
  });

  it('重置为初始值', () => {
    const { result } = renderHook(() => useCounter(10));

    act(() => {
      result.current.increment();
      result.current.increment();
    });

    expect(result.current.count).toBe(12);

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(10);
  });
});

Testing Async Components

测试异步组件

typescript
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
import * as api from './api';

jest.mock('./api');

describe('UserProfile Async', () => {
  it('loads and displays user data', async () => {
    const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' };
    jest.mocked(api.fetchUser).mockResolvedValue(mockUser);

    render(<UserProfile userId={1} />);

    expect(screen.getByText('Loading...')).toBeInTheDocument();

    await waitFor(() => {
      expect(screen.getByText('Alice')).toBeInTheDocument();
    });

    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });

  it('displays error on fetch failure', async () => {
    jest.mocked(api.fetchUser).mockRejectedValue(new Error('Network error'));

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument();
    });
  });
});
typescript
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UserProfile } from './UserProfile';
import * as api from './api';

jest.mock('./api');

describe('异步用户资料组件', () => {
  it('加载并显示用户数据', async () => {
    const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' };
    jest.mocked(api.fetchUser).mockResolvedValue(mockUser);

    render(<UserProfile userId={1} />);

    expect(screen.getByText('Loading...')).toBeInTheDocument();

    await waitFor(() => {
      expect(screen.getByText('Alice')).toBeInTheDocument();
    });

    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });

  it('获取失败时显示错误信息', async () => {
    jest.mocked(api.fetchUser).mockRejectedValue(new Error('Network error'));

    render(<UserProfile userId={1} />);

    await waitFor(() => {
      expect(screen.getByText(/error/i)).toBeInTheDocument();
    });
  });
});

Snapshot Testing

快照测试

Component Snapshots

组件快照

typescript
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';

describe('UserCard Snapshots', () => {
  it('matches snapshot for regular user', () => {
    const { container } = render(
      <UserCard
        name="Alice"
        email="alice@example.com"
        role="user"
      />
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  it('matches snapshot for admin user', () => {
    const { container } = render(
      <UserCard
        name="Bob"
        email="bob@example.com"
        role="admin"
      />
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  it('uses inline snapshot', () => {
    const user = { id: 1, name: 'Charlie', role: 'user' };

    expect(user).toMatchInlineSnapshot(`
      {
        "id": 1,
        "name": "Charlie",
        "role": "user",
      }
    `);
  });
});
typescript
import { render } from '@testing-library/react';
import { UserCard } from './UserCard';

describe('用户卡片快照', () => {
  it('匹配普通用户的快照', () => {
    const { container } = render(
      <UserCard
        name="Alice"
        email="alice@example.com"
        role="user"
      />
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  it('匹配管理员用户的快照', () => {
    const { container } = render(
      <UserCard
        name="Bob"
        email="bob@example.com"
        role="admin"
      />
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  it('使用内联快照', () => {
    const user = { id: 1, name: 'Charlie', role: 'user' };

    expect(user).toMatchInlineSnapshot(`
      {
        "id": 1,
        "name": "Charlie",
        "role": "user",
      }
    `);
  });
});

Updating Snapshots

更新快照

bash
undefined
bash
undefined

Update all snapshots

更新所有快照

jest --updateSnapshot jest -u
jest --updateSnapshot jest -u

Update snapshots for specific test file

更新指定测试文件的快照

jest UserCard.test.tsx -u
jest UserCard.test.tsx -u

Interactive snapshot update

交互式更新快照

jest --watch
jest --watch

Press 'u' to update failing snapshots

按'u'键更新失败的快照

undefined
undefined

Custom Snapshot Serializers

自定义快照序列化器

typescript
// __tests__/serializers/dateSerializer.ts
export default {
  test: (val: any) => val instanceof Date,
  print: (val: Date) => `Date(${val.toISOString()})`,
};
jest.config.ts:
typescript
const config: Config = {
  snapshotSerializers: ['<rootDir>/__tests__/serializers/dateSerializer.ts'],
};
typescript
// __tests__/serializers/dateSerializer.ts
export default {
  test: (val: any) => val instanceof Date,
  print: (val: Date) => `Date(${val.toISOString()})`,
};
jest.config.ts:
typescript
const config: Config = {
  snapshotSerializers: ['<rootDir>/__tests__/serializers/dateSerializer.ts'],
};

Async Testing

异步测试

Testing Promises

测试Promise

typescript
import { fetchData, saveData } from './api';

describe('Async Operations', () => {
  it('resolves with data', async () => {
    const data = await fetchData(1);
    expect(data).toBeDefined();
    expect(data.id).toBe(1);
  });

  it('handles promise rejection', async () => {
    await expect(fetchData(-1)).rejects.toThrow('Invalid ID');
  });

  it('uses resolves matcher', async () => {
    await expect(fetchData(1)).resolves.toHaveProperty('id', 1);
  });

  it('tests multiple async operations', async () => {
    const [user, posts] = await Promise.all([
      fetchUser(1),
      fetchPosts(1),
    ]);

    expect(user.id).toBe(1);
    expect(posts).toHaveLength(expect.any(Number));
  });
});
typescript
import { fetchData, saveData } from './api';

describe('异步操作', () => {
  it('解析并返回数据', async () => {
    const data = await fetchData(1);
    expect(data).toBeDefined();
    expect(data.id).toBe(1);
  });

  it('处理Promise拒绝', async () => {
    await expect(fetchData(-1)).rejects.toThrow('Invalid ID');
  });

  it('使用resolves匹配器', async () => {
    await expect(fetchData(1)).resolves.toHaveProperty('id', 1);
  });

  it('测试多个异步操作', async () => {
    const [user, posts] = await Promise.all([
      fetchUser(1),
      fetchPosts(1),
    ]);

    expect(user.id).toBe(1);
    expect(posts).toHaveLength(expect.any(Number));
  });
});

Testing Callbacks

测试回调函数

typescript
describe('Callback Testing', () => {
  it('calls callback with correct arguments', (done) => {
    function fetchWithCallback(id: number, callback: (data: any) => void) {
      setTimeout(() => {
        callback({ id, name: 'Test' });
      }, 100);
    }

    fetchWithCallback(1, (data) => {
      try {
        expect(data.id).toBe(1);
        expect(data.name).toBe('Test');
        done();
      } catch (error) {
        done(error);
      }
    });
  });
});
typescript
describe('回调测试', () => {
  it('传入正确参数调用回调', (done) => {
    function fetchWithCallback(id: number, callback: (data: any) => void) {
      setTimeout(() => {
        callback({ id, name: 'Test' });
      }, 100);
    }

    fetchWithCallback(1, (data) => {
      try {
        expect(data.id).toBe(1);
        expect(data.name).toBe('Test');
        done();
      } catch (error) {
        done(error);
      }
    });
  });
});

Coverage Configuration

覆盖率配置

Advanced Coverage Setup

高级覆盖率设置

jest.config.ts:
typescript
const config: Config = {
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageProvider: 'v8', // or 'babel' for compatibility
  coverageReporters: ['text', 'lcov', 'html', 'json'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/__tests__/**',
    '!src/index.ts',
    '!src/types/**',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
    './src/core/': {
      branches: 90,
      functions: 90,
      lines: 90,
      statements: 90,
    },
  },
  coveragePathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/__tests__/',
  ],
};
jest.config.ts:
typescript
const config: Config = {
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageProvider: 'v8', // 或使用'babel'以兼容更多场景
  coverageReporters: ['text', 'lcov', 'html', 'json'],
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/__tests__/**',
    '!src/index.ts',
    '!src/types/**',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
    './src/core/': {
      branches: 90,
      functions: 90,
      lines: 90,
      statements: 90,
    },
  },
  coveragePathIgnorePatterns: [
    '/node_modules/',
    '/dist/',
    '/__tests__/',
  ],
};

Running Coverage

运行覆盖率统计

bash
undefined
bash
undefined

Generate coverage report

生成覆盖率报告

npm test -- --coverage
npm test -- --coverage

Coverage with watch mode

监听模式下生成覆盖率报告

npm test -- --coverage --watch
npm test -- --coverage --watch

Coverage for specific files

为指定文件生成覆盖率报告

npm test -- --coverage --collectCoverageFrom="src/components/**/*.tsx"
npm test -- --coverage --collectCoverageFrom="src/components/**/*.tsx"

View HTML report

查看HTML格式报告

open coverage/lcov-report/index.html
undefined
open coverage/lcov-report/index.html
undefined

Migration from Vitest

从Vitest迁移

Key Differences

核心差异

API Changes:
typescript
// Vitest
import { vi } from 'vitest';
const mockFn = vi.fn();
vi.spyOn(obj, 'method');

// Jest
import { jest } from '@jest/globals';
const mockFn = jest.fn();
jest.spyOn(obj, 'method');
API变化:
typescript
// Vitest
import { vi } from 'vitest';
const mockFn = vi.fn();
vi.spyOn(obj, 'method');

// Jest
import { jest } from '@jest/globals';
const mockFn = jest.fn();
jest.spyOn(obj, 'method');

Migration Checklist

迁移清单

1. Update Dependencies:
bash
npm uninstall vitest @vitest/ui
npm install -D jest @types/jest ts-jest
2. Update package.json:
json
{
  "scripts": {
    "test": "jest",           // Was: vitest run
    "test:watch": "jest --watch"  // Was: vitest
  }
}
3. Replace vitest.config.ts with jest.config.ts:
typescript
// Old: vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
  },
});

// New: jest.config.ts
import type { Config } from 'jest';
const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  globals: {
    'ts-jest': {
      isolatedModules: true,
    },
  },
};
export default config;
4. Update Test Files:
typescript
// Change imports
- import { vi } from 'vitest';
+ import { jest } from '@jest/globals';

// Update mocks
- vi.fn()
+ jest.fn()

- vi.spyOn()
+ jest.spyOn()

- vi.mock()
+ jest.mock()

// Timer mocks
- vi.useFakeTimers()
+ jest.useFakeTimers()

- vi.advanceTimersByTime()
+ jest.advanceTimersByTime()
5. Update tsconfig.json:
json
{
  "compilerOptions": {
    "types": ["jest", "@testing-library/jest-dom"]  // Was: vitest/globals
  }
}
1. 更新依赖:
bash
npm uninstall vitest @vitest/ui
npm install -D jest @types/jest ts-jest
2. 更新package.json:
json
{
  "scripts": {
    "test": "jest",           // 原命令: vitest run
    "test:watch": "jest --watch"  // 原命令: vitest
  }
}
3. 替换vitest.config.ts为jest.config.ts:
typescript
// 旧配置: vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
  },
});

// 新配置: jest.config.ts
import type { Config } from 'jest';
const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  globals: {
    'ts-jest': {
      isolatedModules: true,
    },
  },
};
export default config;
4. 更新测试文件:
typescript
// 替换导入语句
- import { vi } from 'vitest';
+ import { jest } from '@jest/globals';

// 更新模拟语法
- vi.fn()
+ jest.fn()

- vi.spyOn()
+ jest.spyOn()

- vi.mock()
+ jest.mock()

// 定时器模拟
- vi.useFakeTimers()
+ jest.useFakeTimers()

- vi.advanceTimersByTime()
+ jest.advanceTimersByTime()
5. 更新tsconfig.json:
json
{
  "compilerOptions": {
    "types": ["jest", "@testing-library/jest-dom"]  // 原配置: vitest/globals
  }
}

Jest vs Vitest Comparison

Jest与Vitest对比

Performance

性能

Jest:
  • Slower initial startup (no HMR)
  • Sequential test execution by default
  • 1-5 seconds for medium projects
Vitest:
  • Instant HMR-based execution
  • Parallel by default
  • 100-500ms for same projects
Jest:
  • 初始启动速度较慢(无HMR)
  • 默认按顺序执行测试
  • 中型项目耗时1-5秒
Vitest:
  • 基于HMR的即时执行
  • 默认并行执行
  • 同类项目耗时100-500毫秒

Ecosystem

生态系统

Jest:
  • ✅ 70% market share
  • ✅ Mature ecosystem (8+ years)
  • ✅ More Stack Overflow answers
  • ✅ Better corporate support
Vitest:
  • ✅ Modern, growing adoption
  • ✅ Vite-native integration
  • ⚠️ Smaller ecosystem
  • ⚠️ Fewer resources
Jest:
  • ✅ 70%市场份额
  • ✅ 成熟生态(8年以上历史)
  • ✅ Stack Overflow上的相关回答更多
  • ✅ 更好的企业级支持
Vitest:
  • ✅ 现代化,采用率持续增长
  • ✅ 原生集成Vite
  • ⚠️ 生态系统规模较小
  • ⚠️ 相关资源较少

TypeScript Support

TypeScript支持

Jest:
  • Requires ts-jest configuration
  • Extra transform step
  • Slower compilation
Vitest:
  • Built-in TypeScript support
  • No configuration needed
  • Faster through Vite
Jest:
  • 需要配置ts-jest
  • 额外的转换步骤
  • 编译速度较慢
Vitest:
  • 内置TypeScript支持
  • 无需额外配置
  • 基于Vite实现更快的编译

When to Use Jest

选择建议

Choose Jest for:
  • ✅ Existing projects already using Jest
  • ✅ Corporate environments requiring proven tools
  • ✅ Projects requiring extensive ecosystem support
  • ✅ React projects with Create React App
  • ✅ Non-Vite build systems (Webpack, Rollup)
Choose Vitest for:
  • ✅ New projects with modern tooling
  • ✅ Vite-based applications
  • ✅ Performance-critical test suites
  • ✅ ESM-first projects
选择Jest的场景:
  • ✅ 已使用Jest的现有项目
  • ✅ 需要成熟工具的企业环境
  • ✅ 需要丰富生态系统支持的项目
  • ✅ 使用Create React App的React项目
  • ✅ 非Vite构建系统(Webpack、Rollup)
选择Vitest的场景:
  • ✅ 使用现代化工具链的新项目
  • ✅ 基于Vite的应用
  • ✅ 对测试执行速度有要求的测试套件
  • ✅ 优先ESM的项目

Best Practices

最佳实践

  1. Use TypeScript Configuration: Type-safe tests prevent runtime errors
  2. Mock External Dependencies: Network, file system, databases
  3. Isolate Tests: Each test should be independent
  4. Use describe Blocks: Group related tests logically
  5. Clear Mock State: Use
    jest.clearAllMocks()
    in
    beforeEach
  6. Test Edge Cases: Empty arrays, null, undefined, errors
  7. Use .each for Data-Driven Tests: Test multiple inputs efficiently
  8. Avoid Testing Implementation: Test behavior, not internal structure
  9. Keep Tests Fast: Mock slow operations, use parallel execution
  10. Maintain Coverage Thresholds: Enforce minimum coverage in CI
  1. 使用TypeScript配置:类型安全的测试可避免运行时错误
  2. 模拟外部依赖:网络、文件系统、数据库等
  3. 隔离测试:每个测试应独立运行
  4. 使用describe块:按逻辑分组相关测试
  5. 清理模拟状态:在
    beforeEach
    中使用
    jest.clearAllMocks()
  6. 测试边界情况:空数组、null、undefined、错误场景
  7. 使用.each实现数据驱动测试:高效测试多组输入
  8. 避免测试实现细节:测试行为而非内部结构
  9. 保持测试快速:模拟慢速操作,使用并行执行
  10. 维护覆盖率阈值:在CI中强制执行最低覆盖率要求

Common Pitfalls

常见陷阱

Not clearing mocks between tests:
typescript
// WRONG - mocks leak between tests
it('test 1', () => {
  jest.spyOn(api, 'fetch');
  // No cleanup!
});

// CORRECT
afterEach(() => {
  jest.restoreAllMocks();
});
Forgetting to await async tests:
typescript
// WRONG - test completes 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();
});
Using wrong test environment:
typescript
// WRONG - testing DOM without jsdom
// jest.config.ts
testEnvironment: 'node',  // Can't test React!

// CORRECT
testEnvironment: 'jsdom',
Not using TypeScript types for mocks:
typescript
// WRONG - no type safety
const mockFn = jest.fn();

// CORRECT
const mockFn = jest.fn<(id: number) => Promise<User>>();
测试间未清理模拟:
typescript
// 错误 - 模拟状态在测试间泄漏
it('测试1', () => {
  jest.spyOn(api, 'fetch');
  // 无清理操作!
});

// 正确做法
afterEach(() => {
  jest.restoreAllMocks();
});
异步测试未使用await:
typescript
// 错误 - 测试在断言前完成
it('获取数据', () => {
  fetchData().then(data => {
    expect(data).toBeDefined();  // 永远不会执行!
  });
});

// 正确做法
it('获取数据', async () => {
  const data = await fetchData();
  expect(data).toBeDefined();
});
使用错误的测试环境:
typescript
// 错误 - 无jsdom环境无法测试DOM
// jest.config.ts
testEnvironment: 'node',  // 无法测试React组件!

// 正确做法
testEnvironment: 'jsdom',
模拟函数未使用TypeScript类型:
typescript
// 错误 - 无类型安全
const mockFn = jest.fn();

// 正确做法
const mockFn = jest.fn<(id: number) => Promise<User>>();

Resources

相关资源

Related Skills

相关技能

When using Jest, consider these complementary skills:
  • typescript-core: Advanced TypeScript patterns, tsconfig optimization, and type safety
  • react: React component testing patterns with Testing Library
  • vitest: Modern alternative with Vite-native performance and faster execution
使用Jest时,可结合以下互补技能:
  • typescript-core: 高级TypeScript模式、tsconfig优化、类型安全
  • react: 结合Testing Library的React组件测试模式
  • vitest: 现代化替代方案,具备Vite原生性能与更快的执行速度

Quick TypeScript Type Safety Reference (Inlined for Standalone Use)

快速TypeScript类型安全参考(独立使用)

typescript
// Type-safe test helpers with generics
function createMockUser<T extends Partial<User>>(overrides: T): User & T {
  return {
    id: 1,
    name: 'Test User',
    email: 'test@example.com',
    ...overrides
  };
}

// Usage with type inference
const adminUser = createMockUser({ role: 'admin' });
// Type: User & { role: string }

// Type-safe mock functions
const mockFetch = jest.fn<typeof fetch>();
mockFetch.mockResolvedValue(new Response('{}'));

// Const type parameters for literal types
const createConfig = <const T extends Record<string, unknown>>(config: T): T => config;
const testConfig = createConfig({ environment: 'test', debug: true });
// Type: { environment: "test"; debug: true } (literals preserved)
typescript
// 带泛型的类型安全测试工具
function createMockUser<T extends Partial<User>>(overrides: T): User & T {
  return {
    id: 1,
    name: 'Test User',
    email: 'test@example.com',
    ...overrides
  };
}

// 带类型推断的使用方式
const adminUser = createMockUser({ role: 'admin' });
// 类型: User & { role: string }

// 类型安全模拟函数
const mockFetch = jest.fn<typeof fetch>();
mockFetch.mockResolvedValue(new Response('{}'));

// 字面量类型的const类型参数
const createConfig = <const T extends Record<string, unknown>>(config: T): T => config;
const testConfig = createConfig({ environment: 'test', debug: true });
// 类型: { environment: "test"; debug: true } (保留字面量类型)

Quick React Testing Patterns (Inlined for Standalone Use)

快速React测试模式参考(独立使用)

typescript
// React Testing Library with Jest
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';

// Component testing pattern
describe('UserProfile', () => {
  it('should display 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();
  });

  it('should handle user interactions', async () => {
    const onSubmit = jest.fn();
    render(<UserForm onSubmit={onSubmit} />);

    // User interactions
    await userEvent.type(screen.getByLabelText('Name'), 'Bob');
    await userEvent.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', () => {
  const { result } = renderHook(() => useCounter(0));

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

// Context and Provider testing
const wrapper = ({ children }: { children: React.ReactNode }) => (
  <AuthProvider>{children}</AuthProvider>
);

test('useAuth hook with context', () => {
  const { result } = renderHook(() => useAuth(), { wrapper });
  expect(result.current.user).toBeDefined();
});
typescript
// Jest结合React Testing Library
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';

// 组件测试模式
describe('用户资料组件', () => {
  it('应显示用户信息', () => {
    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();
  });

  it('应处理用户交互', async () => {
    const onSubmit = jest.fn();
    render(<UserForm onSubmit={onSubmit} />);

    // 用户交互操作
    await userEvent.type(screen.getByLabelText('Name'), 'Bob');
    await userEvent.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);
});

// Context与Provider测试
const wrapper = ({ children }: { children: React.ReactNode }) => (
  <AuthProvider>{children}</AuthProvider>
);

test('结合Context的useAuth Hook', () => {
  const { result } = renderHook(() => useAuth(), { wrapper });
  expect(result.current.user).toBeDefined();
});

Quick Vitest Comparison (Inlined for Standalone Use)

快速Vitest对比参考(独立使用)

When to Choose Vitest over Jest:
  • New Vite/Vite-based projects (Next.js with Turbopack, SvelteKit)
  • Need faster test execution (10-100x faster)
  • ESM-first architecture
  • Hot Module Replacement for tests
When to Stick with Jest:
  • Existing large codebases with Jest already configured
  • Corporate environments with established Jest workflows
  • Need mature ecosystem and extensive plugins
  • React apps with Create React App (default Jest setup)
Migration Snippet (Jest → Vitest):
typescript
// Jest: import from '@testing-library/jest-dom'
import '@testing-library/jest-dom';

// Vitest: import from vitest globals
import { expect, test, describe } from 'vitest';
import { screen } from '@testing-library/react';

// Most Jest syntax works in Vitest unchanged
test('component renders', () => {
  render(<Component />);
  expect(screen.getByText('Hello')).toBeTruthy();
});
[Full TypeScript, React, and Vitest patterns available in respective skills if deployed together]
选择Vitest而非Jest的场景:
  • 新的Vite/Vite生态项目(使用Turbopack的Next.js、SvelteKit)
  • 需要更快的测试执行速度(快10-100倍)
  • 优先ESM的架构
  • 测试支持热模块替换
继续使用Jest的场景:
  • 已配置Jest的大型现有代码库
  • 采用Jest工作流的企业环境
  • 需要成熟生态系统与丰富插件
  • 使用Create React App的React应用(默认Jest配置)
迁移代码片段(Jest → Vitest):
typescript
// Jest: 导入自'@testing-library/jest-dom'
import '@testing-library/jest-dom';

// Vitest: 导入自vitest全局对象
import { expect, test, describe } from 'vitest';
import { screen } from '@testing-library/react';

// 大多数Jest语法可直接在Vitest中使用
test('组件渲染', () => {
  render(<Component />);
  expect(screen.getByText('Hello')).toBeTruthy();
});
[若同时部署,可查看对应技能中的完整TypeScript、React和Vitest模式]

Summary

总结

  • Jest is the industry standard with 70% market share
  • TypeScript support via ts-jest with full type safety
  • All-in-one solution: Test runner, assertions, mocks, coverage
  • React Testing Library integration for component testing
  • Mature ecosystem with extensive tooling and support
  • Snapshot testing for UI regression testing
  • Migration path from Vitest with compatible API
  • Perfect for: Existing projects, corporate environments, React apps, legacy support
  • Trade-off: Slower than Vitest but more mature and widely supported
  • Jest是占据70%市场份额的行业标准测试框架
  • 通过ts-jest实现TypeScript支持,具备完整类型安全
  • 一体化解决方案:集成测试运行器、断言、模拟、覆盖率功能
  • React Testing Library集成:用于组件测试
  • 成熟生态系统:拥有丰富的工具与社区支持
  • 快照测试:用于UI回归测试
  • 提供从Vitest迁移的兼容API路径
  • 适用场景:现有项目、企业环境、React应用、 legacy支持
  • 权衡点:速度慢于Vitest,但生态更成熟、支持更广泛