jest-typescript
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseJest + 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 ReactJest是占据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:initThis 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-jsdomjest.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-jsdomjest.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
undefinedbash
undefinedUpdate 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'键更新失败的快照
undefinedundefinedCustom 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
undefinedbash
undefinedGenerate 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
undefinedopen coverage/lcov-report/index.html
undefinedMigration 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-jest2. 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-jest2. 更新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
最佳实践
- Use TypeScript Configuration: Type-safe tests prevent runtime errors
- Mock External Dependencies: Network, file system, databases
- Isolate Tests: Each test should be independent
- Use describe Blocks: Group related tests logically
- Clear Mock State: Use in
jest.clearAllMocks()beforeEach - Test Edge Cases: Empty arrays, null, undefined, errors
- Use .each for Data-Driven Tests: Test multiple inputs efficiently
- Avoid Testing Implementation: Test behavior, not internal structure
- Keep Tests Fast: Mock slow operations, use parallel execution
- Maintain Coverage Thresholds: Enforce minimum coverage in CI
- 使用TypeScript配置:类型安全的测试可避免运行时错误
- 模拟外部依赖:网络、文件系统、数据库等
- 隔离测试:每个测试应独立运行
- 使用describe块:按逻辑分组相关测试
- 清理模拟状态:在中使用
beforeEachjest.clearAllMocks() - 测试边界情况:空数组、null、undefined、错误场景
- 使用.each实现数据驱动测试:高效测试多组输入
- 避免测试实现细节:测试行为而非内部结构
- 保持测试快速:模拟慢速操作,使用并行执行
- 维护覆盖率阈值:在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
相关资源
- Documentation: https://jestjs.io/docs/getting-started
- TypeScript Guide: https://jestjs.io/docs/getting-started#using-typescript
- ts-jest: https://kulshekhar.github.io/ts-jest/
- React Testing Library: https://testing-library.com/docs/react-testing-library/intro/
- Jest DOM Matchers: https://github.com/testing-library/jest-dom
- 官方文档: https://jestjs.io/docs/getting-started
- TypeScript指南: https://jestjs.io/docs/getting-started#using-typescript
- ts-jest文档: https://kulshekhar.github.io/ts-jest/
- React Testing Library: https://testing-library.com/docs/react-testing-library/intro/
- Jest DOM匹配器: https://github.com/testing-library/jest-dom
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,但生态更成熟、支持更广泛