testing-library

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

React Testing Library

React Testing Library

Status: Production Ready Last Updated: 2026-02-06 Version: 16.x User Event: 14.x

状态:可用于生产环境 最后更新时间:2026-02-06 版本:16.x User Event:14.x

Quick Start

快速开始

bash
undefined
bash
undefined

Install with Vitest

配合Vitest安装

pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom

Or with Jest

或配合Jest安装

pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
undefined
pnpm add -D @testing-library/react @testing-library/user-event @testing-library/jest-dom
undefined

Setup File (src/test/setup.ts)

配置文件(src/test/setup.ts)

typescript
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

// Cleanup after each test
afterEach(() => {
  cleanup();
});
typescript
import '@testing-library/jest-dom/vitest';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

// 每个测试后清理
afterEach(() => {
  cleanup();
});

Vitest Config

Vitest 配置

typescript
// vitest.config.ts
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});

typescript
// vitest.config.ts
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
  },
});

Query Priority (Accessibility First)

查询优先级(以可访问性为先)

Use queries in this order for accessible, resilient tests:
PriorityQueryUse For
1
getByRole
Buttons, links, headings, inputs
2
getByLabelText
Form inputs with labels
3
getByPlaceholderText
Inputs without visible labels
4
getByText
Non-interactive text content
5
getByTestId
Last resort only
按照以下顺序使用查询方法,以编写可访问、更稳定的测试:
优先级查询方法适用场景
1
getByRole
按钮、链接、标题、输入框
2
getByLabelText
带标签的表单输入框
3
getByPlaceholderText
无可见标签的输入框
4
getByText
非交互式文本内容
5
getByTestId
仅作为最后选择

Examples

示例

typescript
import { render, screen } from '@testing-library/react';

// ✅ GOOD - semantic role queries
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('link', { name: /learn more/i });

// ✅ GOOD - label-based queries for forms
screen.getByLabelText(/email address/i);

// ⚠️ OK - when no better option
screen.getByText(/welcome to our app/i);

// ❌ AVOID - not accessible, brittle
screen.getByTestId('submit-button');

typescript
import { render, screen } from '@testing-library/react';

// ✅ 推荐 - 语义化角色查询
screen.getByRole('button', { name: /submit/i });
screen.getByRole('heading', { level: 1 });
screen.getByRole('textbox', { name: /email/i });
screen.getByRole('link', { name: /learn more/i });

// ✅ 推荐 - 表单的标签式查询
screen.getByLabelText(/email address/i);

// ⚠️ 可用 - 没有更好选择时使用
screen.getByText(/welcome to our app/i);

// ❌ 避免 - 不可访问、易断裂
screen.getByTestId('submit-button');

Query Variants

查询变体

VariantReturnsThrowsUse For
getBy
ElementYesElement exists now
queryBy
Element or nullNoElement might not exist
findBy
Promise<Element>YesAsync, appears later
getAllBy
Element[]YesMultiple elements
queryAllBy
Element[]NoMultiple or none
findAllBy
Promise<Element[]>YesMultiple, async
变体返回值是否抛出错误适用场景
getBy
元素元素当前已存在
queryBy
元素或null元素可能不存在
findBy
Promise<Element>异步加载、稍后出现的元素
getAllBy
Element[]多个元素
queryAllBy
Element[]多个或无元素
findAllBy
Promise<Element[]>多个异步加载的元素

When to Use Each

各变体的使用时机

typescript
// Element exists immediately
const button = screen.getByRole('button');

// Check element doesn't exist
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

// Wait for async element to appear
const modal = await screen.findByRole('dialog');

// Multiple elements
const items = screen.getAllByRole('listitem');

typescript
// 元素立即存在
const button = screen.getByRole('button');

// 检查元素是否不存在
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

// 等待异步元素出现
const modal = await screen.findByRole('dialog');

// 多个元素
const items = screen.getAllByRole('listitem');

User Event (Realistic Interactions)

User Event(真实交互模拟)

Always use
userEvent
over
fireEvent
- it simulates real user behavior.
typescript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('Form', () => {
  it('submits form data', async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();

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

    // Type in inputs
    await user.type(screen.getByLabelText(/email/i), 'test@example.com');
    await user.type(screen.getByLabelText(/password/i), 'secret123');

    // Click submit
    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(onSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'secret123',
    });
  });
});
始终使用
userEvent
而非
fireEvent
- 它能模拟真实用户行为。
typescript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('Form', () => {
  it('submits form data', async () => {
    const user = userEvent.setup();
    const onSubmit = vi.fn();

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

    // 在输入框中输入内容
    await user.type(screen.getByLabelText(/email/i), 'test@example.com');
    await user.type(screen.getByLabelText(/password/i), 'secret123');

    // 点击提交按钮
    await user.click(screen.getByRole('button', { name: /sign in/i }));

    expect(onSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'secret123',
    });
  });
});

Common User Events

常见用户事件

typescript
const user = userEvent.setup();

// Clicking
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element); // Select all text

// Typing
await user.type(input, 'hello world');
await user.clear(input);
await user.type(input, '{Enter}'); // Special keys

// Keyboard
await user.keyboard('{Shift>}A{/Shift}'); // Shift+A
await user.tab(); // Tab navigation

// Selection
await user.selectOptions(select, ['option1', 'option2']);

// Hover
await user.hover(element);
await user.unhover(element);

// Clipboard
await user.copy();
await user.paste();

typescript
const user = userEvent.setup();

// 点击
await user.click(element);
await user.dblClick(element);
await user.tripleClick(element); // 全选文本

// 输入
await user.type(input, 'hello world');
await user.clear(input);
await user.type(input, '{Enter}'); // 特殊按键

// 键盘操作
await user.keyboard('{Shift>}A{/Shift}'); // Shift+A
await user.tab(); // Tab导航

// 选择
await user.selectOptions(select, ['option1', 'option2']);

// 悬停
await user.hover(element);
await user.unhover(element);

// 剪贴板
await user.copy();
await user.paste();

Async Testing

异步测试

findBy - Wait for Element

findBy - 等待元素出现

typescript
it('shows loading then content', async () => {
  render(<AsyncComponent />);

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

  // Wait for content to appear (auto-retries)
  const content = await screen.findByText(/data loaded/i);
  expect(content).toBeInTheDocument();
});
typescript
it('shows loading then content', async () => {
  render(<AsyncComponent />);

  // 初始显示加载状态
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // 等待内容出现(自动重试)
  const content = await screen.findByText(/data loaded/i);
  expect(content).toBeInTheDocument();
});

waitFor - Wait for Condition

waitFor - 等待条件满足

typescript
import { waitFor } from '@testing-library/react';

it('updates count after click', async () => {
  const user = userEvent.setup();
  render(<Counter />);

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

  // Wait for state update
  await waitFor(() => {
    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });
});
typescript
import { waitFor } from '@testing-library/react';

it('updates count after click', async () => {
  const user = userEvent.setup();
  render(<Counter />);

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

  // 等待状态更新
  await waitFor(() => {
    expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
  });
});

waitForElementToBeRemoved

waitForElementToBeRemoved

typescript
import { waitForElementToBeRemoved } from '@testing-library/react';

it('hides modal after close', async () => {
  const user = userEvent.setup();
  render(<ModalComponent />);

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

  // Wait for modal to disappear
  await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});

typescript
import { waitForElementToBeRemoved } from '@testing-library/react';

it('hides modal after close', async () => {
  const user = userEvent.setup();
  render(<ModalComponent />);

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

  // 等待模态框消失
  await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});

MSW Integration (API Mocking)

MSW集成(API模拟)

Mock API calls at the network level with Mock Service Worker.
bash
pnpm add -D msw
通过Mock Service Worker在网络层模拟API调用。
bash
pnpm add -D msw

Setup (src/test/mocks/handlers.ts)

配置(src/test/mocks/handlers.ts)

typescript
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/user', () => {
    return HttpResponse.json({
      id: 1,
      name: 'Test User',
      email: 'test@example.com',
    });
  }),

  http.post('/api/login', async ({ request }) => {
    const body = await request.json();
    if (body.password === 'correct') {
      return HttpResponse.json({ token: 'abc123' });
    }
    return HttpResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    );
  }),
];
typescript
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/user', () => {
    return HttpResponse.json({
      id: 1,
      name: 'Test User',
      email: 'test@example.com',
    });
  }),

  http.post('/api/login', async ({ request }) => {
    const body = await request.json();
    if (body.password === 'correct') {
      return HttpResponse.json({ token: 'abc123' });
    }
    return HttpResponse.json(
      { error: 'Invalid credentials' },
      { status: 401 }
    );
  }),
];

Setup (src/test/mocks/server.ts)

配置(src/test/mocks/server.ts)

typescript
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
typescript
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);

Test Setup

测试配置

typescript
// src/test/setup.ts
import { server } from './mocks/server';
import { beforeAll, afterEach, afterAll } from 'vitest';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
typescript
// src/test/setup.ts
import { server } from './mocks/server';
import { beforeAll, afterEach, afterAll } from 'vitest';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Using in Tests

在测试中使用

typescript
import { server } from '../test/mocks/server';
import { http, HttpResponse } from 'msw';

it('handles API error', async () => {
  // Override handler for this test
  server.use(
    http.get('/api/user', () => {
      return HttpResponse.json(
        { error: 'Server error' },
        { status: 500 }
      );
    })
  );

  render(<UserProfile />);

  await screen.findByText(/error loading user/i);
});

typescript
import { server } from '../test/mocks/server';
import { http, HttpResponse } from 'msw';

it('handles API error', async () => {
  // 为当前测试覆盖处理器
  server.use(
    http.get('/api/user', () => {
      return HttpResponse.json(
        { error: 'Server error' },
        { status: 500 }
      );
    })
  );

  render(<UserProfile />);

  await screen.findByText(/error loading user/i);
});

Accessibility Testing

可访问性测试

Check for A11y Violations

检查可访问性违规

bash
pnpm add -D @axe-core/react
typescript
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
bash
pnpm add -D @axe-core/react
typescript
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('has no accessibility violations', async () => {
  const { container } = render(<MyComponent />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});

Role-Based Queries Are A11y Tests

基于角色的查询本身就是可访问性测试

Using
getByRole
implicitly tests accessibility:
typescript
// This passes only if button is properly accessible
screen.getByRole('button', { name: /submit/i });

// Fails if:
// - Element isn't a button or role="button"
// - Accessible name doesn't match
// - Element is hidden from accessibility tree

使用
getByRole
会隐式测试可访问性:
typescript
// 只有当按钮具备正确的可访问性时,此查询才会通过
screen.getByRole('button', { name: /submit/i });

// 在以下情况会失败:
// - 元素不是按钮或未设置role="button"
// - 可访问名称不匹配
// - 元素在可访问性树中被隐藏

Testing Patterns

测试模式

Forms

表单测试

typescript
it('validates required fields', async () => {
  const user = userEvent.setup();
  render(<ContactForm />);

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

  // Check for validation errors
  expect(screen.getByText(/email is required/i)).toBeInTheDocument();
  expect(screen.getByText(/message is required/i)).toBeInTheDocument();
});
typescript
it('validates required fields', async () => {
  const user = userEvent.setup();
  render(<ContactForm />);

  // 未填写必填字段就提交
  await user.click(screen.getByRole('button', { name: /submit/i }));

  // 检查验证错误提示
  expect(screen.getByText(/email is required/i)).toBeInTheDocument();
  expect(screen.getByText(/message is required/i)).toBeInTheDocument();
});

Modals/Dialogs

模态框测试

typescript
it('opens and closes modal', async () => {
  const user = userEvent.setup();
  render(<ModalTrigger />);

  // Modal not visible initially
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

  // Open modal
  await user.click(screen.getByRole('button', { name: /open/i }));
  expect(screen.getByRole('dialog')).toBeInTheDocument();

  // Close modal
  await user.click(screen.getByRole('button', { name: /close/i }));
  await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});
typescript
it('opens and closes modal', async () => {
  const user = userEvent.setup();
  render(<ModalTrigger />);

  // 初始状态下模态框不可见
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

  // 打开模态框
  await user.click(screen.getByRole('button', { name: /open/i }));
  expect(screen.getByRole('dialog')).toBeInTheDocument();

  // 关闭模态框
  await user.click(screen.getByRole('button', { name: /close/i }));
  await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
});

Lists

列表测试

typescript
it('renders list items', () => {
  render(<TodoList items={['Buy milk', 'Walk dog']} />);

  const items = screen.getAllByRole('listitem');
  expect(items).toHaveLength(2);
  expect(items[0]).toHaveTextContent('Buy milk');
});

typescript
it('renders list items', () => {
  render(<TodoList items={['Buy milk', 'Walk dog']} />);

  const items = screen.getAllByRole('listitem');
  expect(items).toHaveLength(2);
  expect(items[0]).toHaveTextContent('Buy milk');
});

Common Matchers (jest-dom)

常用匹配器(jest-dom)

typescript
// Presence
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEmptyDOMElement();

// State
expect(button).toBeEnabled();
expect(button).toBeDisabled();
expect(checkbox).toBeChecked();
expect(input).toBeRequired();

// Content
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveValue('test');
expect(element).toHaveAttribute('href', '/about');

// Styles
expect(element).toHaveClass('active');
expect(element).toHaveStyle({ color: 'red' });

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

typescript
// 存在性
expect(element).toBeInTheDocument();
expect(element).toBeVisible();
expect(element).toBeEmptyDOMElement();

// 状态
expect(button).toBeEnabled();
expect(button).toBeDisabled();
expect(checkbox).toBeChecked();
expect(input).toBeRequired();

// 内容
expect(element).toHaveTextContent(/hello/i);
expect(element).toHaveValue('test');
expect(element).toHaveAttribute('href', '/about');

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

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

Debugging

相关链接

screen.debug()

typescript
it('debugs rendering', () => {
  render(<MyComponent />);

  // Print entire DOM
  screen.debug();

  // Print specific element
  screen.debug(screen.getByRole('button'));
});

logRoles

typescript
import { logRoles } from '@testing-library/react';

it('shows available roles', () => {
  const { container } = render(<MyComponent />);
  logRoles(container);
});

Common Mistakes

Using getBy for Async

typescript
// ❌ WRONG - fails if element appears async
const modal = screen.getByRole('dialog');

// ✅ CORRECT - waits for element
const modal = await screen.findByRole('dialog');

Not Awaiting User Events

typescript
// ❌ WRONG - race condition
user.click(button);
expect(result).toBeInTheDocument();

// ✅ CORRECT - await the interaction
await user.click(button);
expect(result).toBeInTheDocument();

Using container.querySelector

typescript
// ❌ WRONG - not accessible, brittle
const button = container.querySelector('.submit-btn');

// ✅ CORRECT - accessible query
const button = screen.getByRole('button', { name: /submit/i });

See Also