When it comes to testing React apps manually, we can either choose to render individual component trees in a simplified test environment or run the complete app in a realistic browser environment (end-to-end testing). But for automated tests, React Testing Library (RTL) is recommended for its user-centric approach and maintainability.
React Testing Library is built on top of DOM Testing Library to test React components by querying and interacting with real DOM nodes, avoiding reliance on implementation details.
RTL addresses maintainability by focusing on user-visible behavior:
data-testid
as an escape hatch when needed.Component (App.js):
const title = 'Hello, World!';
function App() {
return <div>{title}</div>;
}
export default App;
Test (App.test.js):
import { render } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
});
});
Add debug to inspect output:
import { render, screen } from '@testing-library/react';
import App from './App';
describe('App', () => {
test('renders App component', () => {
render(<App />);
screen.debug();
});
});
Output in console:
<body>
<div>
<div>Hello, World!</div>
</div>
</body>
getByText
, getByAltText
, etc.).import { render, screen } from '@testing-library/react';
test('should show login form', () => {
render(<Login />);
const input = screen.getByLabelText('Username');
// events & assertions
});
Single element queries:
getBy*
: throws if none foundqueryBy*
: returns null if nonefindBy*
: async PromiseMultiple elements queries:
getAllBy*
: throws if nonequeryAllBy*
: returns [] if nonefindAllBy*
: async Promise arraydescribe
, test
, expect
).// 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
optionsimport { 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: {
/* custom queries */
}
});
expect(container).toBeInTheDocument();
});
renderHook
usageimport { 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');
// Override testId attribute
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];
}
Or using 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 jest
setupFiles
: import '@testing-library/react/dont-cleanup-after-each';
shallow
/mount
with 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();
});
This comprehensive cheat sheet covers basic to advanced RTL usage—rendering, querying, mocking, custom queries, and integration tests—to help you write robust, maintainable tests.