精选 在手动测试 React 应用时,我们可以选择在简化的测试环境中渲染单个组件树,也可以在真实的浏览器环境中运行完整的应用(端到端测试)。但对于自动化测试,推荐使用 React Testing Library (RTL),因为它采用以用户为中心的设计方式并且易于维护。
React Testing Library 构建于 DOM Testing Library 之上,通过查询和操作真实 DOM 节点来测试 React 组件,避免对组件内部实现细节产生依赖。
RTL 通过关注用户可见的行为来提升测试的可维护性:
data-testid 作为保底回退选择。受测组件 (App.js):
const title = 'Hello, World!';
function App() {
return <div>{title}</div>;
}
export default App;
测试脚本 (App.test.js):
import { render } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
});
});
添加 debug 方法查看渲染 DOM 树:
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.debug();
});
});
控制台输出内容 (Console Output):
<body>
<div>
<div>Hello, World!</div>
</div>
</body>
getByText, getByAltText 等)。import { render, screen } from '@testing-library/react';
test('should show login form', () => {
render(<Login />);
const input = screen.getByLabelText('Username');
// 事件交互与断言校验
});
单元素查询选择器 (Single element queries):
getBy*: 找不到匹配元素时直接抛出 Exception 异常queryBy*: 找不到匹配元素时返回 nullfindBy*: 返回异步 Promise多元素查询选择器 (Multiple elements queries):
getAllBy*: 找不到匹配元素时抛出异常queryAllBy*: 找不到匹配元素时返回空数组 []findAllBy*: 返回包含元素的异步 Promise 数组describe, test, expect 等 API)。// fetch.test.jsx
import React from 'react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import Fetch from '../fetch';
const server = setupServer(
rest.get('/greeting', (req, res, ctx) =>
res(ctx.json({ greeting: 'hello there' }))
)
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('loads and displays greeting', async () => {
render(<Fetch url="/greeting" />);
fireEvent.click(screen.getByText('Load Greeting'));
await waitFor(() => screen.getByRole('heading'));
expect(screen.getByRole('heading')).toHaveTextContent('hello there');
expect(screen.getByRole('button')).toBeDisabled();
});
test('handles server error', async () => {
server.use(rest.get('/greeting', (req, res, ctx) => res(ctx.status(500))));
render(<Fetch url="/greeting" />);
fireEvent.click(screen.getByText('Load Greeting'));
await waitFor(() => screen.getByRole('alert'));
expect(screen.getByRole('alert')).toHaveTextContent('Oops, failed to fetch!');
expect(screen.getByRole('button')).not.toBeDisabled();
});
render 函数高级配置项 (render options)import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
test('renders a message', () => {
const table = document.createElement('table');
const { container } = render(<TableBody {...props} />, {
container: document.body.appendChild(table),
baseElement: document.body,
hydrate: true,
legacyRoot: true,
queries: {
/* 自定义查询选择器 */
}
});
expect(container).toBeInTheDocument();
});
renderHook 测试自定义 Hook (renderHook usage)import { renderHook } from '@testing-library/react';
test('returns logged in user', () => {
const { result, rerender } = renderHook(
({ name } = {}) => useLoggedInUser(name),
{ initialProps: { name: 'Alice' } }
);
expect(result.current).toEqual({ name: 'Alice' });
rerender({ name: 'Bob' });
expect(result.current).toEqual({ name: 'Bob' });
});
const dom = require('@testing-library/dom');
const { queryHelpers, buildQueries } = require('@testing-library/react');
// 重写覆盖默认的 testId 属性名称
export const queryByTestId = queryHelpers.queryByAttribute.bind(
null,
'data-test-id'
);
export const queryAllByTestId = queryHelpers.queryAllByAttribute.bind(
null,
'data-test-id'
);
export function getAllByTestId(container, id, ...rest) {
const els = queryAllByTestId(container, id, ...rest);
if (!els.length)
throw queryHelpers.getElementError(
`No element with [data-test-id="${id}"]`,
container
);
return els;
}
export function getByTestId(container, id, ...rest) {
const els = getAllByTestId(container, id, ...rest);
if (els.length > 1)
throw queryHelpers.getElementError(
`Multiple elements with [data-test-id="${id}"]`,
container
);
return els[0];
}
或者使用 buildQueries 进行构建:
const queryAllByDataCy = (...args) =>
queryHelpers.queryAllByAttribute('data-cy', ...args);
const [
queryByDataCy,
getAllByDataCy,
getByDataCy,
findAllByDataCy,
findByDataCy
] = buildQueries(
queryAllByDataCy,
(c, v) => `Found multiple elements with data-cy="${v}"`,
(c, v) => `Unable to find element with data-cy="${v}"`
);
cross-env RTL_SKIP_AUTO_CLEANUP=true jestsetupFiles:import '@testing-library/react/dont-cleanup-after-each';jest-dom。shallow/mount 替换为 render + screen。import { render, within } from '@testing-library/react';
const { getByText } = render(<MyComponent />);
const section = getByText('messages');
const hello = within(section).getByText('hello');
import { render, cleanup, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import nock from 'nock';
import App from '../App';
const REPOS = [{ name: 'repo1', description: '...' }];
beforeAll(() =>
nock('https://api.github.com')
.persist()
.get('/users/alice/repos')
.reply(200, REPOS)
);
afterEach(cleanup);
test('user sees public repos', async () => {
render(<App />);
userEvent.type(screen.getByPlaceholderText('Enter username'), 'alice');
userEvent.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() =>
REPOS.forEach((r) => expect(screen.getByText(r.name)).toBeInTheDocument())
);
expect(screen.queryByText('Loading...')).toBeNull();
});
本快速备忘手册涵盖了从基础到高级的 RTL 用法——渲染、查询选择器、网络 Mock 拦截、自定义 Query 选择器以及集成测试,帮助您编写稳健、易于维护的测试套件。