jest-react-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Jest React Testing

Jest React 测试

A comprehensive skill for testing React applications using Jest and React Testing Library. This skill covers everything from basic component testing to advanced patterns including mocking, async testing, custom hooks testing, and integration testing strategies.
这是一项使用Jest和React Testing Library测试React应用的全面技能。本技能涵盖了从基础组件测试到高级模式的所有内容,包括模拟、异步测试、自定义Hooks测试和集成测试策略。

When to Use This Skill

何时使用本技能

Use this skill when:
  • Testing React components with Jest and React Testing Library
  • Setting up Jest configuration for React projects
  • Writing unit tests for components, hooks, and utilities
  • Testing user interactions and component behavior
  • Mocking modules, functions, API calls, and external dependencies
  • Testing asynchronous operations (API calls, timers, promises)
  • Testing custom React hooks
  • Writing integration tests for complex component trees
  • Debugging failing tests or improving test coverage
  • Following testing best practices and patterns
在以下场景使用本技能:
  • 使用Jest和React Testing Library测试React组件
  • 为React项目配置Jest
  • 为组件、Hooks和工具函数编写单元测试
  • 测试用户交互和组件行为
  • 模拟模块、函数、API调用和外部依赖
  • 测试异步操作(API调用、定时器、Promise)
  • 测试自定义React Hooks
  • 为复杂组件树编写集成测试
  • 调试失败的测试或提高测试覆盖率
  • 遵循测试最佳实践和模式

Core Concepts

核心概念

Testing Philosophy

测试理念

React Testing Library follows these guiding principles:
  • Test User Behavior, Not Implementation: Write tests that resemble how users interact with your app
  • Accessibility First: Use queries that promote accessible components (getByRole, getByLabelText)
  • Avoid Testing Implementation Details: Don't test state, props, or internal methods directly
  • Maintainable Tests: Tests should break when behavior changes, not when code refactors
  • Confidence Over Coverage: Focus on tests that give confidence, not 100% coverage
React Testing Library遵循以下指导原则:
  • 测试用户行为,而非实现细节:编写与用户和应用交互方式一致的测试
  • 可访问性优先:使用支持无障碍组件的查询方法(getByRole、getByLabelText)
  • 避免测试实现细节:不要直接测试状态、Props或内部方法
  • 可维护的测试:当行为变化时测试应该失败,而不是代码重构时
  • 信心优先于覆盖率:专注于能提供信心的测试,而非追求100%覆盖率

Key Testing Concepts

关键测试概念

  1. Queries: Methods to find elements (getBy, queryBy, findBy)
  2. User Events: Simulating user interactions (click, type, select)
  3. Async Testing: Testing components with asynchronous operations
  4. Mocking: Replacing dependencies with controlled test doubles
  5. Assertions: Verifying expected outcomes with matchers
  1. 查询方法:查找元素的方法(getBy、queryBy、findBy)
  2. 用户事件:模拟用户交互(点击、输入、选择)
  3. 异步测试:测试包含异步操作的组件
  4. 模拟:用可控的测试替身替换依赖项
  5. 断言:使用匹配器验证预期结果

Jest Configuration

Jest 配置

Basic Jest Configuration

基础Jest配置

jest.config.js (JavaScript projects):
javascript
/** @type {import('jest').Config} */
const config = {
  // Test environment for DOM testing
  testEnvironment: 'jsdom',

  // Setup files after environment
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],

  // Module paths
  moduleDirectories: ['node_modules', 'src'],

  // Transform files with babel-jest
  transform: {
    '^.+\\.(js|jsx)$': 'babel-jest',
  },

  // Module name mapper for static assets and CSS
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
  },

  // Coverage configuration
  collectCoverageFrom: [
    'src/**/*.{js,jsx}',
    '!src/index.js',
    '!src/**/*.test.{js,jsx}',
    '!src/**/__tests__/**',
  ],

  // Coverage thresholds
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

module.exports = config;
jest.config.js (TypeScript projects):
typescript
import type {Config} from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],

  moduleDirectories: ['node_modules', 'src'],

  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },

  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.ts',
    '^@/(.*)$': '<rootDir>/src/$1',
  },

  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/index.tsx',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/__tests__/**',
    '!src/**/*.d.ts',
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

export default config;
jest.config.js(JavaScript项目):
javascript
/** @type {import('jest').Config} */
const config = {
  // DOM测试的测试环境
  testEnvironment: 'jsdom',

  // 环境初始化后的设置文件
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],

  // 模块路径
  moduleDirectories: ['node_modules', 'src'],

  // 使用babel-jest转换文件
  transform: {
    '^.+\\.(js|jsx)$': 'babel-jest',
  },

  // 静态资源和CSS的模块名称映射
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js',
  },

  // 覆盖率配置
  collectCoverageFrom: [
    'src/**/*.{js,jsx}',
    '!src/index.js',
    '!src/**/*.test.{js,jsx}',
    '!src/**/__tests__/**',
  ],

  // 覆盖率阈值
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

module.exports = config;
jest.config.js(TypeScript项目):
typescript
import type {Config} from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],

  moduleDirectories: ['node_modules', 'src'],

  transform: {
    '^.+\\.tsx?$': 'ts-jest',
  },

  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.ts',
    '^@/(.*)$': '<rootDir>/src/$1',
  },

  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/index.tsx',
    '!src/**/*.test.{ts,tsx}',
    '!src/**/__tests__/**',
    '!src/**/*.d.ts',
  ],

  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

export default config;

Setup Files

设置文件

src/setupTests.js:
javascript
// Add custom jest matchers from jest-dom
import '@testing-library/jest-dom';

// Extend expect with jest-extended matchers (optional)
import * as matchers from 'jest-extended';
expect.extend(matchers);

// Mock window.matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

// Mock IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
  constructor() {}
  disconnect() {}
  observe() {}
  takeRecords() {
    return [];
  }
  unobserve() {}
};

// Suppress console errors in tests (optional)
const originalError = console.error;
beforeAll(() => {
  console.error = (...args) => {
    if (
      typeof args[0] === 'string' &&
      args[0].includes('Warning: ReactDOM.render')
    ) {
      return;
    }
    originalError.call(console, ...args);
  };
});

afterAll(() => {
  console.error = originalError;
});

// Reset mocks after each test
afterEach(() => {
  jest.clearAllMocks();
});
src/setupTests.js
javascript
// 从jest-dom添加自定义jest匹配器
import '@testing-library/jest-dom';

// (可选)使用jest-extended匹配器扩展expect
import * as matchers from 'jest-extended';
expect.extend(matchers);

// 模拟window.matchMedia
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: jest.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  })),
});

// 模拟IntersectionObserver
global.IntersectionObserver = class IntersectionObserver {
  constructor() {}
  disconnect() {}
  observe() {}
  takeRecords() {
    return [];
  }
  unobserve() {}
};

// (可选)在测试中抑制控制台错误
const originalError = console.error;
beforeAll(() => {
  console.error = (...args) => {
    if (
      typeof args[0] === 'string' &&
      args[0].includes('Warning: ReactDOM.render')
    ) {
      return;
    }
    originalError.call(console, ...args);
  };
});

afterAll(() => {
  console.error = originalError;
});

// 每次测试后重置模拟
afterEach(() => {
  jest.clearAllMocks();
});

File Mocks

文件模拟

mocks/fileMock.js:
javascript
module.exports = 'test-file-stub';
mocks/styleMock.js:
javascript
module.exports = {};
mocks/fileMock.js
javascript
module.exports = 'test-file-stub';
mocks/styleMock.js
javascript
module.exports = {};

React Testing Library Queries

React Testing Library 查询方法

Query Types

查询类型

React Testing Library provides three types of queries:
  1. getBy: Returns element or throws error (use for elements that should exist)
  2. queryBy: Returns element or null (use for elements that may not exist)
  3. findBy: Returns promise that resolves to element (use for async elements)
React Testing Library提供三种查询类型:
  1. getBy:返回元素或抛出错误(用于应该存在的元素)
  2. queryBy:返回元素或null(用于可能不存在的元素)
  3. findBy:返回解析为元素的Promise(用于异步元素)

Query Priority

查询优先级

Recommended Query Order (accessibility-focused):
  1. getByRole: Most accessible query
    javascript
    getByRole('button', { name: /submit/i })
    getByRole('heading', { level: 1 })
    getByRole('textbox', { name: /username/i })
  2. getByLabelText: For form fields with labels
    javascript
    getByLabelText(/email address/i)
    getByLabelText('Password')
  3. getByPlaceholderText: For inputs with placeholders
    javascript
    getByPlaceholderText(/search/i)
  4. getByText: For non-interactive elements with text
    javascript
    getByText(/welcome/i)
    getByText('Error: Invalid credentials')
  5. getByDisplayValue: For form elements with values
    javascript
    getByDisplayValue('John Doe')
  6. getByAltText: For images with alt text
    javascript
    getByAltText(/profile picture/i)
  7. getByTitle: For elements with title attribute
    javascript
    getByTitle(/close/i)
  8. getByTestId: Last resort when other queries don't work
    javascript
    getByTestId('custom-element')
推荐查询顺序(以可访问性为核心):
  1. getByRole:最符合无障碍标准的查询
    javascript
    getByRole('button', { name: /submit/i })
    getByRole('heading', { level: 1 })
    getByRole('textbox', { name: /username/i })
  2. getByLabelText:用于带标签的表单字段
    javascript
    getByLabelText(/email address/i)
    getByLabelText('Password')
  3. getByPlaceholderText:用于带占位符的输入框
    javascript
    getByPlaceholderText(/search/i)
  4. getByText:用于带文本的非交互式元素
    javascript
    getByText(/welcome/i)
    getByText('Error: Invalid credentials')
  5. getByDisplayValue:用于带值的表单元素
    javascript
    getByDisplayValue('John Doe')
  6. getByAltText:用于带替代文本的图片
    javascript
    getByAltText(/profile picture/i)
  7. getByTitle:用于带title属性的元素
    javascript
    getByTitle(/close/i)
  8. getByTestId:其他查询方法无效时的最后选择
    javascript
    getByTestId('custom-element')

Query Variants

查询变体

javascript
// Single element queries
screen.getByRole('button')      // Throws if not found or multiple found
screen.queryByRole('button')    // Returns null if not found
await screen.findByRole('button') // Async, waits up to 1000ms

// Multiple element queries
screen.getAllByRole('listitem')      // Throws if none found
screen.queryAllByRole('listitem')    // Returns [] if none found
await screen.findAllByRole('listitem') // Async version
javascript
// 单个元素查询
screen.getByRole('button')      // 未找到或找到多个时抛出错误
screen.queryByRole('button')    // 未找到时返回null
await screen.findByRole('button') // 异步方法,最多等待1000ms

// 多个元素查询
screen.getAllByRole('listitem')      // 未找到任何元素时抛出错误
screen.queryAllByRole('listitem')    // 未找到时返回[]
await screen.findAllByRole('listitem') // 异步版本

Component Testing Strategies

组件测试策略

Basic Component Test

基础组件测试

javascript
import { render, screen } from '@testing-library/react';
import { Greeting } from './Greeting';

describe('Greeting Component', () => {
  it('renders greeting message', () => {
    render(<Greeting name="Alice" />);

    expect(screen.getByText(/hello, alice/i)).toBeInTheDocument();
  });

  it('renders default greeting when no name provided', () => {
    render(<Greeting />);

    expect(screen.getByText(/hello, guest/i)).toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import { Greeting } from './Greeting';

describe('Greeting Component', () => {
  it('渲染问候消息', () => {
    render(<Greeting name="Alice" />);

    expect(screen.getByText(/hello, alice/i)).toBeInTheDocument();
  });

  it('未提供名称时渲染默认问候语', () => {
    render(<Greeting />);

    expect(screen.getByText(/hello, guest/i)).toBeInTheDocument();
  });
});

Testing User Interactions

测试用户交互

javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter Component', () => {
  it('increments counter on button click', async () => {
    const user = userEvent.setup();
    render(<Counter />);

    const button = screen.getByRole('button', { name: /increment/i });
    const count = screen.getByText(/count: 0/i);

    expect(count).toBeInTheDocument();

    await user.click(button);

    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });

  it('decrements counter on button click', async () => {
    const user = userEvent.setup();
    render(<Counter initialCount={5} />);

    const decrementBtn = screen.getByRole('button', { name: /decrement/i });

    await user.click(decrementBtn);

    expect(screen.getByText(/count: 4/i)).toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';

describe('Counter Component', () => {
  it('点击按钮时增加计数器值', async () => {
    const user = userEvent.setup();
    render(<Counter />);

    const button = screen.getByRole('button', { name: /increment/i });
    const count = screen.getByText(/count: 0/i);

    expect(count).toBeInTheDocument();

    await user.click(button);

    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });

  it('点击按钮时减少计数器值', async () => {
    const user = userEvent.setup();
    render(<Counter initialCount={5} />);

    const decrementBtn = screen.getByRole('button', { name: /decrement/i });

    await user.click(decrementBtn);

    expect(screen.getByText(/count: 4/i)).toBeInTheDocument();
  });
});

Testing Forms

测试表单

javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

describe('LoginForm Component', () => {
  it('submits form with username and password', async () => {
    const user = userEvent.setup();
    const handleSubmit = jest.fn();

    render(<LoginForm onSubmit={handleSubmit} />);

    const usernameInput = screen.getByLabelText(/username/i);
    const passwordInput = screen.getByLabelText(/password/i);
    const submitButton = screen.getByRole('button', { name: /submit/i });

    await user.type(usernameInput, 'testuser');
    await user.type(passwordInput, 'password123');
    await user.click(submitButton);

    expect(handleSubmit).toHaveBeenCalledTimes(1);
    expect(handleSubmit).toHaveBeenCalledWith({
      username: 'testuser',
      password: 'password123',
    });
  });

  it('shows validation errors for empty fields', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={jest.fn()} />);

    const submitButton = screen.getByRole('button', { name: /submit/i });

    await user.click(submitButton);

    expect(screen.getByText(/username is required/i)).toBeInTheDocument();
    expect(screen.getByText(/password is required/i)).toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

describe('LoginForm Component', () => {
  it('提交包含用户名和密码的表单', async () => {
    const user = userEvent.setup();
    const handleSubmit = jest.fn();

    render(<LoginForm onSubmit={handleSubmit} />);

    const usernameInput = screen.getByLabelText(/username/i);
    const passwordInput = screen.getByLabelText(/password/i);
    const submitButton = screen.getByRole('button', { name: /submit/i });

    await user.type(usernameInput, 'testuser');
    await user.type(passwordInput, 'password123');
    await user.click(submitButton);

    expect(handleSubmit).toHaveBeenCalledTimes(1);
    expect(handleSubmit).toHaveBeenCalledWith({
      username: 'testuser',
      password: 'password123',
    });
  });

  it('为空字段显示验证错误', async () => {
    const user = userEvent.setup();
    render(<LoginForm onSubmit={jest.fn()} />);

    const submitButton = screen.getByRole('button', { name: /submit/i });

    await user.click(submitButton);

    expect(screen.getByText(/username is required/i)).toBeInTheDocument();
    expect(screen.getByText(/password is required/i)).toBeInTheDocument();
  });
});

Testing Conditional Rendering

测试条件渲染

javascript
import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';

describe('UserProfile Component', () => {
  it('shows loading state when loading', () => {
    render(<UserProfile loading={true} />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();
    expect(screen.queryByRole('heading')).not.toBeInTheDocument();
  });

  it('shows user data when loaded', () => {
    const user = {
      name: 'John Doe',
      email: 'john@example.com',
    };

    render(<UserProfile loading={false} user={user} />);

    expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
    expect(screen.getByRole('heading', { name: /john doe/i })).toBeInTheDocument();
    expect(screen.getByText(/john@example.com/i)).toBeInTheDocument();
  });

  it('shows error message when error occurs', () => {
    render(<UserProfile loading={false} error="Failed to load user" />);

    expect(screen.getByText(/failed to load user/i)).toBeInTheDocument();
    expect(screen.queryByRole('heading')).not.toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import { UserProfile } from './UserProfile';

describe('UserProfile Component', () => {
  it('加载时显示加载状态', () => {
    render(<UserProfile loading={true} />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();
    expect(screen.queryByRole('heading')).not.toBeInTheDocument();
  });

  it('加载完成后显示用户数据', () => {
    const user = {
      name: 'John Doe',
      email: 'john@example.com',
    };

    render(<UserProfile loading={false} user={user} />);

    expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
    expect(screen.getByRole('heading', { name: /john doe/i })).toBeInTheDocument();
    expect(screen.getByText(/john@example.com/i)).toBeInTheDocument();
  });

  it('发生错误时显示错误消息', () => {
    render(<UserProfile loading={false} error="Failed to load user" />);

    expect(screen.getByText(/failed to load user/i)).toBeInTheDocument();
    expect(screen.queryByRole('heading')).not.toBeInTheDocument();
  });
});

Mocking Patterns

模拟模式

Mocking Modules

模拟模块

Automatic Mock:
javascript
// __mocks__/axios.js
export default {
  get: jest.fn(),
  post: jest.fn(),
  put: jest.fn(),
  delete: jest.fn(),
};
Usage in test:
javascript
import axios from 'axios';
import { UserService } from './UserService';

jest.mock('axios');

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

  it('fetches users successfully', async () => {
    const mockUsers = [{ id: 1, name: 'Alice' }];
    axios.get.mockResolvedValue({ data: mockUsers });

    const users = await UserService.getUsers();

    expect(axios.get).toHaveBeenCalledWith('/api/users');
    expect(users).toEqual(mockUsers);
  });

  it('handles fetch error', async () => {
    axios.get.mockRejectedValue(new Error('Network Error'));

    await expect(UserService.getUsers()).rejects.toThrow('Network Error');
  });
});
自动模拟:
javascript
// __mocks__/axios.js
export default {
  get: jest.fn(),
  post: jest.fn(),
  put: jest.fn(),
  delete: jest.fn(),
};
测试中的用法:
javascript
import axios from 'axios';
import { UserService } from './UserService';

jest.mock('axios');

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

  it('成功获取用户', async () => {
    const mockUsers = [{ id: 1, name: 'Alice' }];
    axios.get.mockResolvedValue({ data: mockUsers });

    const users = await UserService.getUsers();

    expect(axios.get).toHaveBeenCalledWith('/api/users');
    expect(users).toEqual(mockUsers);
  });

  it('处理获取数据时的错误', async () => {
    axios.get.mockRejectedValue(new Error('Network Error'));

    await expect(UserService.getUsers()).rejects.toThrow('Network Error');
  });
});

Mocking Functions

模拟函数

javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button Component', () => {
  it('calls onClick handler when clicked', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();

    render(<Button onClick={handleClick}>Click Me</Button>);

    await user.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('calls onClick with event object', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();

    render(<Button onClick={handleClick}>Click Me</Button>);

    await user.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'click',
      })
    );
  });
});
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button Component', () => {
  it('点击时调用onClick处理函数', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();

    render(<Button onClick={handleClick}>Click Me</Button>);

    await user.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('调用onClick时传入事件对象', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();

    render(<Button onClick={handleClick}>Click Me</Button>);

    await user.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledWith(
      expect.objectContaining({
        type: 'click',
      })
    );
  });
});

Mocking API Calls with MSW (Mock Service Worker)

使用MSW(Mock Service Worker)模拟API调用

Setup MSW:
javascript
// src/mocks/handlers.js
import { rest } from 'msw';

export const handlers = [
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ])
    );
  }),

  rest.post('/api/users', async (req, res, ctx) => {
    const { name, email } = await req.json();

    return res(
      ctx.status(201),
      ctx.json({
        id: 3,
        name,
        email,
      })
    );
  }),
];
Setup server:
javascript
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
Configure in setupTests.js:
javascript
import { server } from './mocks/server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Usage in tests:
javascript
import { render, screen, waitFor } from '@testing-library/react';
import { server } from './mocks/server';
import { rest } from 'msw';
import { UserList } from './UserList';

describe('UserList Component', () => {
  it('fetches and displays users', async () => {
    render(<UserList />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();

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

  it('handles server error', async () => {
    server.use(
      rest.get('/api/users', (req, res, ctx) => {
        return res(
          ctx.status(500),
          ctx.json({ message: 'Internal Server Error' })
        );
      })
    );

    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText(/error loading users/i)).toBeInTheDocument();
    });
  });
});
配置MSW:
javascript
// src/mocks/handlers.js
import { rest } from 'msw';

export const handlers = [
  rest.get('/api/users', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        { id: 1, name: 'Alice' },
        { id: 2, name: 'Bob' },
      ])
    );
  }),

  rest.post('/api/users', async (req, res, ctx) => {
    const { name, email } = await req.json();

    return res(
      ctx.status(201),
      ctx.json({
        id: 3,
        name,
        email,
      })
    );
  }),
];
配置服务器:
javascript
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
在setupTests.js中配置:
javascript
import { server } from './mocks/server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
在测试中使用:
javascript
import { render, screen, waitFor } from '@testing-library/react';
import { server } from './mocks/server';
import { rest } from 'msw';
import { UserList } from './UserList';

describe('UserList Component', () => {
  it('获取并显示用户', async () => {
    render(<UserList />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();

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

  it('处理服务器错误', async () => {
    server.use(
      rest.get('/api/users', (req, res, ctx) => {
        return res(
          ctx.status(500),
          ctx.json({ message: 'Internal Server Error' })
        );
      })
    );

    render(<UserList />);

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

Mocking Context

模拟上下文

javascript
import { render, screen } from '@testing-library/react';
import { AuthContext } from './AuthContext';
import { ProtectedComponent } from './ProtectedComponent';

const mockAuthContext = (overrides = {}) => ({
  user: { id: 1, name: 'Test User' },
  isAuthenticated: true,
  login: jest.fn(),
  logout: jest.fn(),
  ...overrides,
});

describe('ProtectedComponent', () => {
  it('renders content for authenticated user', () => {
    const contextValue = mockAuthContext();

    render(
      <AuthContext.Provider value={contextValue}>
        <ProtectedComponent />
      </AuthContext.Provider>
    );

    expect(screen.getByText(/welcome, test user/i)).toBeInTheDocument();
  });

  it('renders login prompt for unauthenticated user', () => {
    const contextValue = mockAuthContext({
      user: null,
      isAuthenticated: false,
    });

    render(
      <AuthContext.Provider value={contextValue}>
        <ProtectedComponent />
      </AuthContext.Provider>
    );

    expect(screen.getByText(/please log in/i)).toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import { AuthContext } from './AuthContext';
import { ProtectedComponent } from './ProtectedComponent';

const mockAuthContext = (overrides = {}) => ({
  user: { id: 1, name: 'Test User' },
  isAuthenticated: true,
  login: jest.fn(),
  logout: jest.fn(),
  ...overrides,
});

describe('ProtectedComponent', () => {
  it('为已认证用户渲染内容', () => {
    const contextValue = mockAuthContext();

    render(
      <AuthContext.Provider value={contextValue}>
        <ProtectedComponent />
      </AuthContext.Provider>
    );

    expect(screen.getByText(/welcome, test user/i)).toBeInTheDocument();
  });

  it('为未认证用户渲染登录提示', () => {
    const contextValue = mockAuthContext({
      user: null,
      isAuthenticated: false,
    });

    render(
      <AuthContext.Provider value={contextValue}>
        <ProtectedComponent />
      </AuthContext.Provider>
    );

    expect(screen.getByText(/please log in/i)).toBeInTheDocument();
  });
});

Mocking Child Components

模拟子组件

javascript
import { render, screen } from '@testing-library/react';
import { ParentComponent } from './ParentComponent';

// Mock the child component
jest.mock('./ChildComponent', () => ({
  ChildComponent: ({ title, onAction }) => (
    <div>
      <h2>{title}</h2>
      <button onClick={onAction}>Mocked Action</button>
    </div>
  ),
}));

describe('ParentComponent', () => {
  it('renders with mocked child', () => {
    render(<ParentComponent />);

    expect(screen.getByText(/mocked action/i)).toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import { ParentComponent } from './ParentComponent';

// 模拟子组件
jest.mock('./ChildComponent', () => ({
  ChildComponent: ({ title, onAction }) => (
    <div>
      <h2>{title}</h2>
      <button onClick={onAction}>Mocked Action</button>
    </div>
  ),
}));

describe('ParentComponent', () => {
  it('使用模拟的子组件渲染', () => {
    render(<ParentComponent />);

    expect(screen.getByText(/mocked action/i)).toBeInTheDocument();
  });
});

Async Testing Patterns

异步测试模式

Testing with waitFor

使用waitFor测试

javascript
import { render, screen, waitFor } from '@testing-library/react';
import { AsyncComponent } from './AsyncComponent';

describe('AsyncComponent', () => {
  it('loads and displays data', async () => {
    render(<AsyncComponent />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();

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

  it('waits for specific condition', async () => {
    render(<AsyncComponent />);

    await waitFor(
      () => {
        expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
      },
      { timeout: 3000 }
    );
  });
});
javascript
import { render, screen, waitFor } from '@testing-library/react';
import { AsyncComponent } from './AsyncComponent';

describe('AsyncComponent', () => {
  it('加载并显示数据', async () => {
    render(<AsyncComponent />);

    expect(screen.getByText(/loading/i)).toBeInTheDocument();

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

  it('等待特定条件满足', async () => {
    render(<AsyncComponent />);

    await waitFor(
      () => {
        expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
      },
      { timeout: 3000 }
    );
  });
});

Testing with findBy Queries

使用findBy查询测试

javascript
import { render, screen } from '@testing-library/react';
import { DataFetcher } from './DataFetcher';

describe('DataFetcher Component', () => {
  it('displays fetched data', async () => {
    render(<DataFetcher />);

    // findBy automatically waits for element to appear
    const heading = await screen.findByRole('heading', { name: /data/i });
    expect(heading).toBeInTheDocument();
  });

  it('handles timeout for missing elements', async () => {
    render(<DataFetcher url="/api/missing" />);

    await expect(
      screen.findByText(/success/i, {}, { timeout: 500 })
    ).rejects.toThrow();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import { DataFetcher } from './DataFetcher';

describe('DataFetcher Component', () => {
  it('显示获取到的数据', async () => {
    render(<DataFetcher />);

    // findBy会自动等待元素出现
    const heading = await screen.findByRole('heading', { name: /data/i });
    expect(heading).toBeInTheDocument();
  });

  it('等待不存在的元素时超时', async () => {
    render(<DataFetcher url="/api/missing" />);

    await expect(
      screen.findByText(/success/i, {}, { timeout: 500 })
    ).rejects.toThrow();
  });
});

Testing Promises

测试Promise

javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AsyncForm } from './AsyncForm';

describe('AsyncForm Component', () => {
  it('submits form and shows success message', async () => {
    const user = userEvent.setup();
    render(<AsyncForm />);

    const input = screen.getByLabelText(/name/i);
    const submitBtn = screen.getByRole('button', { name: /submit/i });

    await user.type(input, 'John Doe');
    await user.click(submitBtn);

    const successMsg = await screen.findByText(/submitted successfully/i);
    expect(successMsg).toBeInTheDocument();
  });

  it('shows error message on failure', async () => {
    const user = userEvent.setup();
    render(<AsyncForm shouldFail={true} />);

    const submitBtn = screen.getByRole('button', { name: /submit/i });
    await user.click(submitBtn);

    const errorMsg = await screen.findByRole('alert');
    expect(errorMsg).toHaveTextContent(/submission failed/i);
  });
});
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AsyncForm } from './AsyncForm';

describe('AsyncForm Component', () => {
  it('提交表单并显示成功消息', async () => {
    const user = userEvent.setup();
    render(<AsyncForm />);

    const input = screen.getByLabelText(/name/i);
    const submitBtn = screen.getByRole('button', { name: /submit/i });

    await user.type(input, 'John Doe');
    await user.click(submitBtn);

    const successMsg = await screen.findByText(/submitted successfully/i);
    expect(successMsg).toBeInTheDocument();
  });

  it('提交失败时显示错误消息', async () => {
    const user = userEvent.setup();
    render(<AsyncForm shouldFail={true} />);

    const submitBtn = screen.getByRole('button', { name: /submit/i });
    await user.click(submitBtn);

    const errorMsg = await screen.findByRole('alert');
    expect(errorMsg).toHaveTextContent(/submission failed/i);
  });
});

Testing with Fake Timers

使用假定时器测试

javascript
import { render, screen, act } from '@testing-library/react';
import { Timer } from './Timer';

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

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

  it('updates timer every second', () => {
    render(<Timer />);

    expect(screen.getByText(/0 seconds/i)).toBeInTheDocument();

    act(() => {
      jest.advanceTimersByTime(1000);
    });

    expect(screen.getByText(/1 second/i)).toBeInTheDocument();

    act(() => {
      jest.advanceTimersByTime(3000);
    });

    expect(screen.getByText(/4 seconds/i)).toBeInTheDocument();
  });

  it('cleans up timer on unmount', () => {
    const { unmount } = render(<Timer />);

    const clearIntervalSpy = jest.spyOn(global, 'clearInterval');

    unmount();

    expect(clearIntervalSpy).toHaveBeenCalled();
  });
});
javascript
import { render, screen, act } from '@testing-library/react';
import { Timer } from './Timer';

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

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

  it('每秒更新定时器', () => {
    render(<Timer />);

    expect(screen.getByText(/0 seconds/i)).toBeInTheDocument();

    act(() => {
      jest.advanceTimersByTime(1000);
    });

    expect(screen.getByText(/1 second/i)).toBeInTheDocument();

    act(() => {
      jest.advanceTimersByTime(3000);
    });

    expect(screen.getByText(/4 seconds/i)).toBeInTheDocument();
  });

  it('卸载时清理定时器', () => {
    const { unmount } = render(<Timer />);

    const clearIntervalSpy = jest.spyOn(global, 'clearInterval');

    unmount();

    expect(clearIntervalSpy).toHaveBeenCalled();
  });
});

Testing Custom Hooks

测试自定义Hooks

Basic Hook Testing

基础Hook测试

javascript
import { renderHook } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter Hook', () => {
  it('initializes with default value', () => {
    const { result } = renderHook(() => useCounter());

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

  it('initializes with provided value', () => {
    const { result } = renderHook(() => useCounter(10));

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

  it('increments count', () => {
    const { result } = renderHook(() => useCounter());

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

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

  it('decrements count', () => {
    const { result } = renderHook(() => useCounter(5));

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

    expect(result.current.count).toBe(4);
  });
});
javascript
import { renderHook } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter Hook', () => {
  it('使用默认值初始化', () => {
    const { result } = renderHook(() => useCounter());

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

  it('使用提供的值初始化', () => {
    const { result } = renderHook(() => useCounter(10));

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

  it('增加计数器值', () => {
    const { result } = renderHook(() => useCounter());

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

Testing Hooks with Props

测试带Props的Hooks

javascript
import { renderHook } from '@testing-library/react';
import { useFetch } from './useFetch';

describe('useFetch Hook', () => {
  it('fetches data for given URL', async () => {
    const { result } = renderHook(() => useFetch('/api/users'));

    expect(result.current.loading).toBe(true);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toBeDefined();
    expect(result.current.error).toBeNull();
  });

  it('refetches when URL changes', async () => {
    const { result, rerender } = renderHook(
      ({ url }) => useFetch(url),
      { initialProps: { url: '/api/users' } }
    );

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    const firstData = result.current.data;

    rerender({ url: '/api/posts' });

    await waitFor(() => {
      expect(result.current.data).not.toEqual(firstData);
    });
  });
});
javascript
import { renderHook } from '@testing-library/react';
import { useFetch } from './useFetch';

describe('useFetch Hook', () => {
  it('为给定URL获取数据', async () => {
    const { result } = renderHook(() => useFetch('/api/users'));

    expect(result.current.loading).toBe(true);

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toBeDefined();
    expect(result.current.error).toBeNull();
  });

  it('URL变化时重新获取数据', async () => {
    const { result, rerender } = renderHook(
      ({ url }) => useFetch(url),
      { initialProps: { url: '/api/users' } }
    );

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    const firstData = result.current.data;

    rerender({ url: '/api/posts' });

    await waitFor(() => {
      expect(result.current.data).not.toEqual(firstData);
    });
  });
});

Testing Hooks with Context

测试带上下文的Hooks

javascript
import { renderHook } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { useTheme } from './useTheme';

describe('useTheme Hook', () => {
  const wrapper = ({ children }) => (
    <ThemeProvider initialTheme="light">
      {children}
    </ThemeProvider>
  );

  it('returns current theme', () => {
    const { result } = renderHook(() => useTheme(), { wrapper });

    expect(result.current.theme).toBe('light');
  });

  it('toggles theme', () => {
    const { result } = renderHook(() => useTheme(), { wrapper });

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

    expect(result.current.theme).toBe('dark');
  });
});
javascript
import { renderHook } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import { useTheme } from './useTheme';

describe('useTheme Hook', () => {
  const wrapper = ({ children }) => (
    <ThemeProvider initialTheme="light">
      {children}
    </ThemeProvider>
  );

  it('返回当前主题', () => {
    const { result } = renderHook(() => useTheme(), { wrapper });

    expect(result.current.theme).toBe('light');
  });

  it('切换主题', () => {
    const { result } = renderHook(() => useTheme(), { wrapper });

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

    expect(result.current.theme).toBe('dark');
  });
});

Testing Async Hooks

测试异步Hooks

javascript
import { renderHook, waitFor } from '@testing-library/react';
import { useAsyncData } from './useAsyncData';

describe('useAsyncData Hook', () => {
  it('loads data asynchronously', async () => {
    const { result } = renderHook(() => useAsyncData('/api/data'));

    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBeNull();

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toBeDefined();
  });

  it('handles errors', async () => {
    const { result } = renderHook(() => useAsyncData('/api/error'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.error).toBeDefined();
    expect(result.current.data).toBeNull();
  });
});
javascript
import { renderHook, waitFor } from '@testing-library/react';
import { useAsyncData } from './useAsyncData';

describe('useAsyncData Hook', () => {
  it('异步加载数据', async () => {
    const { result } = renderHook(() => useAsyncData('/api/data'));

    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBeNull();

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toBeDefined();
  });

  it('处理错误', async () => {
    const { result } = renderHook(() => useAsyncData('/api/error'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.error).toBeDefined();
    expect(result.current.data).toBeNull();
  });
});

Integration Testing Patterns

集成测试模式

Testing Component Integration

测试组件集成

javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { App } from './App';

describe('App Integration', () => {
  it('navigates between pages', async () => {
    const user = userEvent.setup();
    render(<App />);

    expect(screen.getByText(/home page/i)).toBeInTheDocument();

    const aboutLink = screen.getByRole('link', { name: /about/i });
    await user.click(aboutLink);

    expect(screen.getByText(/about page/i)).toBeInTheDocument();
  });

  it('completes full user flow', async () => {
    const user = userEvent.setup();
    render(<App />);

    // Navigate to signup
    await user.click(screen.getByRole('link', { name: /sign up/i }));

    // Fill out form
    await user.type(screen.getByLabelText(/email/i), 'user@example.com');
    await user.type(screen.getByLabelText(/password/i), 'password123');

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

    // Verify success
    expect(await screen.findByText(/welcome/i)).toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { App } from './App';

describe('App Integration', () => {
  it('在页面之间导航', async () => {
    const user = userEvent.setup();
    render(<App />);

    expect(screen.getByText(/home page/i)).toBeInTheDocument();

    const aboutLink = screen.getByRole('link', { name: /about/i });
    await user.click(aboutLink);

    expect(screen.getByText(/about page/i)).toBeInTheDocument();
  });

  it('完成完整用户流程', async () => {
    const user = userEvent.setup();
    render(<App />);

    // 导航到注册页面
    await user.click(screen.getByRole('link', { name: /sign up/i }));

    // 填写表单
    await user.type(screen.getByLabelText(/email/i), 'user@example.com');
    await user.type(screen.getByLabelText(/password/i), 'password123');

    // 提交表单
    await user.click(screen.getByRole('button', { name: /submit/i }));

    // 验证成功
    expect(await screen.findByText(/welcome/i)).toBeInTheDocument();
  });
});

Testing with Router

测试路由

javascript
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { AppRoutes } from './AppRoutes';

const renderWithRouter = (ui, { initialEntries = ['/'] } = {}) => {
  return render(
    <MemoryRouter initialEntries={initialEntries}>
      {ui}
    </MemoryRouter>
  );
};

describe('AppRoutes Integration', () => {
  it('renders home page by default', () => {
    renderWithRouter(<AppRoutes />);

    expect(screen.getByText(/home/i)).toBeInTheDocument();
  });

  it('renders user page at /users/:id', () => {
    renderWithRouter(<AppRoutes />, { initialEntries: ['/users/123'] });

    expect(screen.getByText(/user profile/i)).toBeInTheDocument();
  });

  it('navigates programmatically', async () => {
    const user = userEvent.setup();
    renderWithRouter(<AppRoutes />);

    const navButton = screen.getByRole('button', { name: /go to profile/i });
    await user.click(navButton);

    expect(screen.getByText(/profile page/i)).toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { AppRoutes } from './AppRoutes';

const renderWithRouter = (ui, { initialEntries = ['/'] } = {}) => {
  return render(
    <MemoryRouter initialEntries={initialEntries}>
      {ui}
    </MemoryRouter>
  );
};

describe('AppRoutes Integration', () => {
  it('默认渲染首页', () => {
    renderWithRouter(<AppRoutes />);

    expect(screen.getByText(/home/i)).toBeInTheDocument();
  });

  it('在/users/:id路径渲染用户页面', () => {
    renderWithRouter(<AppRoutes />, { initialEntries: ['/users/123'] });

    expect(screen.getByText(/user profile/i)).toBeInTheDocument();
  });

  it('编程式导航', async () => {
    const user = userEvent.setup();
    renderWithRouter(<AppRoutes />);

    const navButton = screen.getByRole('button', { name: /go to profile/i });
    await user.click(navButton);

    expect(screen.getByText(/profile page/i)).toBeInTheDocument();
  });
});

Testing with Redux

测试Redux

javascript
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import userEvent from '@testing-library/user-event';
import { TodoList } from './TodoList';
import todosReducer from './todosSlice';

const createMockStore = (initialState = {}) => {
  return configureStore({
    reducer: {
      todos: todosReducer,
    },
    preloadedState: initialState,
  });
};

const renderWithStore = (ui, { store = createMockStore() } = {}) => {
  return render(<Provider store={store}>{ui}</Provider>);
};

describe('TodoList Integration', () => {
  it('adds new todo', async () => {
    const user = userEvent.setup();
    renderWithStore(<TodoList />);

    const input = screen.getByPlaceholderText(/new todo/i);
    const addButton = screen.getByRole('button', { name: /add/i });

    await user.type(input, 'Buy groceries');
    await user.click(addButton);

    expect(screen.getByText(/buy groceries/i)).toBeInTheDocument();
  });

  it('renders initial todos from store', () => {
    const initialState = {
      todos: {
        items: [
          { id: 1, text: 'Existing todo', completed: false },
        ],
      },
    };

    renderWithStore(<TodoList />, { store: createMockStore(initialState) });

    expect(screen.getByText(/existing todo/i)).toBeInTheDocument();
  });
});
javascript
import { render, screen } from '@testing-library/react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import userEvent from '@testing-library/user-event';
import { TodoList } from './TodoList';
import todosReducer from './todosSlice';

const createMockStore = (initialState = {}) => {
  return configureStore({
    reducer: {
      todos: todosReducer,
    },
    preloadedState: initialState,
  });
};

const renderWithStore = (ui, { store = createMockStore() } = {}) => {
  return render(<Provider store={store}>{ui}</Provider>);
};

describe('TodoList Integration', () => {
  it('添加新待办事项', async () => {
    const user = userEvent.setup();
    renderWithStore(<TodoList />);

    const input = screen.getByPlaceholderText(/new todo/i);
    const addButton = screen.getByRole('button', { name: /add/i });

    await user.type(input, 'Buy groceries');
    await user.click(addButton);

    expect(screen.getByText(/buy groceries/i)).toBeInTheDocument();
  });

  it('渲染来自store的初始待办事项', () => {
    const initialState = {
      todos: {
        items: [
          { id: 1, text: 'Existing todo', completed: false },
        ],
      },
    };

    renderWithStore(<TodoList />, { store: createMockStore(initialState) });

    expect(screen.getByText(/existing todo/i)).toBeInTheDocument();
  });
});

Jest DOM Matchers

Jest DOM 匹配器

Common Matchers

常用匹配器

javascript
// Element presence
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();

// Visibility
expect(element).toBeVisible();
expect(element).not.toBeVisible();

// Text content
expect(element).toHaveTextContent('Hello World');
expect(element).toHaveTextContent(/hello/i);

// Attributes
expect(element).toHaveAttribute('type', 'submit');
expect(element).toHaveAttribute('disabled');

// Classes
expect(element).toHaveClass('active');
expect(element).toHaveClass('btn', 'btn-primary');

// Styles
expect(element).toHaveStyle({ color: 'red' });
expect(element).toHaveStyle('display: none');

// Forms
expect(input).toHaveValue('test');
expect(input).toHaveDisplayValue('Test');
expect(checkbox).toBeChecked();
expect(checkbox).not.toBeChecked();
expect(input).toBeDisabled();
expect(input).toBeEnabled();
expect(input).toBeRequired();
expect(input).toBeInvalid();
expect(input).toBeValid();

// Focus
expect(element).toHaveFocus();

// Accessibility
expect(element).toHaveAccessibleName('Submit button');
expect(element).toHaveAccessibleDescription('Click to submit form');

// Contains
expect(container).toContainElement(child);
expect(container).toContainHTML('<span>Text</span>');
javascript
// 元素存在性
expect(element).toBeInTheDocument();
expect(element).not.toBeInTheDocument();

// 可见性
expect(element).toBeVisible();
expect(element).not.toBeVisible();

// 文本内容
expect(element).toHaveTextContent('Hello World');
expect(element).toHaveTextContent(/hello/i);

// 属性
expect(element).toHaveAttribute('type', 'submit');
expect(element).toHaveAttribute('disabled');

// 类名
expect(element).toHaveClass('active');
expect(element).toHaveClass('btn', 'btn-primary');

// 样式
expect(element).toHaveStyle({ color: 'red' });
expect(element).toHaveStyle('display: none');

// 表单
expect(input).toHaveValue('test');
expect(input).toHaveDisplayValue('Test');
expect(checkbox).toBeChecked();
expect(checkbox).not.toBeChecked();
expect(input).toBeDisabled();
expect(input).toBeEnabled();
expect(input).toBeRequired();
expect(input).toBeInvalid();
expect(input).toBeValid();

// 焦点
expect(element).toHaveFocus();

// 可访问性
expect(element).toHaveAccessibleName('Submit button');
expect(element).toHaveAccessibleDescription('Click to submit form');

// 包含关系
expect(container).toContainElement(child);
expect(container).toContainHTML('<span>Text</span>');

Best Practices

最佳实践

Test Organization

测试组织

  1. Group Related Tests: Use
    describe
    blocks to organize tests
    javascript
    describe('UserProfile', () => {
      describe('when loading', () => {
        it('shows loading spinner', () => {});
      });
    
      describe('when loaded', () => {
        it('displays user information', () => {});
        it('shows profile picture', () => {});
      });
    });
  2. Use Descriptive Test Names: Test names should describe behavior
    javascript
    // Good
    it('displays error message when login fails', () => {});
    
    // Bad
    it('test login', () => {});
  3. Follow AAA Pattern: Arrange, Act, Assert
    javascript
    it('increments counter', async () => {
      // Arrange
      const user = userEvent.setup();
      render(<Counter />);
    
      // Act
      await user.click(screen.getByRole('button', { name: /increment/i }));
    
      // Assert
      expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
    });
  1. 分组相关测试:使用
    describe
    块组织测试
    javascript
    describe('UserProfile', () => {
      describe('加载时', () => {
        it('显示加载动画', () => {});
      });
    
      describe('加载完成后', () => {
        it('显示用户信息', () => {});
        it('显示头像', () => {});
      });
    });
  2. 使用描述性的测试名称:测试名称应描述行为
    javascript
    // 好的命名
    it('登录失败时显示错误消息', () => {});
    
    // 不好的命名
    it('测试登录', () => {});
  3. 遵循AAA模式:准备(Arrange)、执行(Act)、断言(Assert)
    javascript
    it('增加计数器值', async () => {
      // 准备
      const user = userEvent.setup();
      render(<Counter />);
    
      // 执行
      await user.click(screen.getByRole('button', { name: /increment/i }));
    
      // 断言
      expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
    });

Query Best Practices

查询最佳实践

  1. Prefer Accessible Queries: Use getByRole, getByLabelText
  2. Use Screen Queries: Import from screen instead of destructuring render
  3. Avoid getByTestId: Use it as last resort only
  4. Use Regular Expressions: More flexible than exact strings
  1. 优先使用可访问的查询方法:使用getByRole、getByLabelText
  2. 使用Screen查询:从screen导入查询方法,而非从render解构
  3. 避免使用getByTestId:仅在其他查询方法无效时使用
  4. 使用正则表达式:比精确字符串更灵活

Async Testing Best Practices

异步测试最佳实践

  1. Use findBy for Async: Prefer findBy over getBy + waitFor
  2. Set Proper Timeouts: Configure waitFor timeouts for slow operations
  3. Avoid act() Warnings: Use userEvent, waitFor, findBy appropriately
  4. Clean Up Timers: Use jest.useFakeTimers() and cleanup properly
  1. 使用findBy处理异步场景:优先使用findBy而非getBy + waitFor
  2. 设置适当的超时时间:为慢操作配置waitFor超时时间
  3. 避免act()警告:正确使用userEvent、waitFor、findBy
  4. 清理定时器:正确使用jest.useFakeTimers()并清理

Mocking Best Practices

模拟最佳实践

  1. Mock at Right Level: Mock external dependencies, not internal logic
  2. Reset Mocks: Clear mocks between tests
  3. Use MSW for API: Prefer MSW over mocking axios/fetch directly
  4. Avoid Over-Mocking: Don't mock what you're testing
  1. 在正确层级模拟:模拟外部依赖,而非内部逻辑
  2. 重置模拟:在测试之间清除模拟
  3. 使用MSW模拟API:优先使用MSW而非直接模拟axios/fetch
  4. 避免过度模拟:不要模拟你要测试的内容

Coverage Best Practices

覆盖率最佳实践

  1. Focus on Behavior: Test user-facing behavior, not implementation
  2. Don't Chase 100%: Focus on critical paths
  3. Test Error States: Include error handling tests
  4. Test Edge Cases: Include boundary conditions
  1. 专注于行为:测试用户可见的行为,而非实现细节
  2. 不追求100%覆盖率:专注于关键路径
  3. 测试错误状态:包含错误处理的测试
  4. 测试边界情况:包含边界条件测试

Common Testing Patterns

常见测试模式

Testing Lists and Iterations

测试列表和迭代

javascript
it('renders list of items', () => {
  const items = ['Apple', 'Banana', 'Cherry'];
  render(<ItemList items={items} />);

  items.forEach(item => {
    expect(screen.getByText(item)).toBeInTheDocument();
  });
});
javascript
it('渲染项目列表', () => {
  const items = ['Apple', 'Banana', 'Cherry'];
  render(<ItemList items={items} />);

  items.forEach(item => {
    expect(screen.getByText(item)).toBeInTheDocument();
  });
});

Testing Accessibility

测试可访问性

javascript
it('has accessible form', () => {
  render(<ContactForm />);

  const nameInput = screen.getByLabelText(/name/i);
  expect(nameInput).toHaveAccessibleName('Name');
  expect(nameInput).toBeRequired();

  const submitButton = screen.getByRole('button', { name: /submit/i });
  expect(submitButton).toHaveAttribute('type', 'submit');
});
javascript
it('表单符合可访问性标准', () => {
  render(<ContactForm />);

  const nameInput = screen.getByLabelText(/name/i);
  expect(nameInput).toHaveAccessibleName('Name');
  expect(nameInput).toBeRequired();

  const submitButton = screen.getByRole('button', { name: /submit/i });
  expect(submitButton).toHaveAttribute('type', 'submit');
});

Testing Error Boundaries

测试错误边界

javascript
it('catches errors and displays fallback', () => {
  const ThrowError = () => {
    throw new Error('Test error');
  };

  // Suppress console.error for this test
  const spy = jest.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary fallback={<div>Something went wrong</div>}>
      <ThrowError />
    </ErrorBoundary>
  );

  expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();

  spy.mockRestore();
});
javascript
it('捕获错误并显示回退内容', () => {
  const ThrowError = () => {
    throw new Error('Test error');
  };

  // 在此测试中抑制console.error
  const spy = jest.spyOn(console, 'error').mockImplementation(() => {});

  render(
    <ErrorBoundary fallback={<div>出现错误</div>}>
      <ThrowError />
    </ErrorBoundary>
  );

  expect(screen.getByText(/出现错误/i)).toBeInTheDocument();

  spy.mockRestore();
});

Testing Portals

测试Portals

javascript
it('renders modal in portal', () => {
  render(<Modal isOpen={true}>Modal Content</Modal>);

  const modal = screen.getByText(/modal content/i);
  expect(modal).toBeInTheDocument();

  // Modal should be in document.body, not in the component tree
  expect(modal.parentElement).toBe(document.body);
});
javascript
it('在Portal中渲染模态框', () => {
  render(<Modal isOpen={true}>Modal Content</Modal>);

  const modal = screen.getByText(/modal content/i);
  expect(modal).toBeInTheDocument();

  // 模态框应在document.body中,而非组件树内
  expect(modal.parentElement).toBe(document.body);
});

Troubleshooting

故障排除

Common Issues

常见问题

Issue: "Unable to find element"
  • Solution: Use screen.debug() to see DOM, check query type, wait for async updates
Issue: "Act warnings"
  • Solution: Use userEvent instead of fireEvent, wrap state updates in act(), use waitFor/findBy
Issue: "Jest timeout"
  • Solution: Increase timeout, check for infinite loops, ensure async operations complete
Issue: "Cannot read property of undefined"
  • Solution: Check mocks are set up correctly, ensure components receive required props
Issue: "Multiple elements found"
  • Solution: Make queries more specific, use getAllBy for multiple elements
问题:“无法找到元素”
  • 解决方案:使用screen.debug()查看DOM,检查查询类型,等待异步更新
问题:“act警告”
  • 解决方案:使用userEvent而非fireEvent,将状态更新包裹在act()中,正确使用waitFor/findBy
问题:“Jest超时”
  • 解决方案:增加超时时间,检查是否存在无限循环,确保异步操作完成
问题:“无法读取未定义的属性”
  • 解决方案:检查模拟是否正确设置,确保组件接收到所需的Props
问题:“找到多个元素”
  • 解决方案:使查询更具体,对多个元素使用getAllBy

Resources

资源


Skill Version: 1.0.0 Last Updated: October 2025 Skill Category: Testing, React, Quality Assurance Compatible With: Jest 29+, React Testing Library 13+, React 16.8+

技能版本:1.0.0 最后更新:2025年10月 技能分类:测试、React、质量保证 兼容版本:Jest 29+、React Testing Library 13+、React 16.8+