mrkeyoor.com_
Wed 05 Aug 05:01 UTC
npmTestingupdated 05 Aug 2026

@testing-library/react

React Testing Library renders your React components into a DOM (usually jsdom) and gives you queries that find elements the way a user would: by role, label, or visible text. The whole point is testing behavior instead of implementation details, so refactors that do not change what the user sees do not break tests. It is the de facto standard for React component tests and what the React docs themselves point to.

Verdict

If you test React components in a Node test runner, this is the correct default and the alternatives are worse. Just accept the philosophy: if you find yourself reaching for internals, you either restructure the test or move that case to a real browser.

API stability4/5The query API has been stable for years; major bumps mostly track React itself. v16 did break setups by making @testing-library/dom a peer dependency you install yourself.
Docs5/5testing-library.com is excellent: query priority guide, common mistakes article, framework-specific examples, and an active FAQ. The 'which query do I use' page answers the question everyone has.
Maintenance4/5Actively maintained under the testing-library org with prompt React 19 support and 82 open issues and PRs; releases are steady though the surface is intentionally small and slow-moving.
Ecosystem5/551M weekly downloads, the whole @testing-library family (user-event, jest-dom, hooks patterns), first-class docs in Jest, Vitest, and the React docs, and every CI template assumes it.

Use it if

  • You write unit or integration tests for React components in Jest or Vitest and want tests that survive refactors
  • You want accessibility pressure built into testing: getByRole fails when your markup has no proper roles or labels, which catches real a11y problems
  • You test user flows inside a component tree (fill form, click submit, assert the result) without paying for a real browser
  • You are on React 18 or 19; v16 supports both and tracks new React versions quickly
Skip it if

Setup reality

Since v16 you install @testing-library/react plus @testing-library/dom yourself as a peer dependency, and @types/react / @types/react-dom on TypeScript projects; forgetting the dom package is the classic post-upgrade error. You still need a test runner with a jsdom environment (jest-environment-jsdom or Vitest with environment: 'jsdom'), @testing-library/jest-dom for readable matchers, and usually a custom render wrapper for your providers. None of it is hard, all of it is fiddly, and every project rebuilds the same setup file.

Patterns

Render a component and assert on outputrender-and-query

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

test('shows greeting', () => {
  render(<Greeting name="Ada" />);
  expect(screen.getByRole('heading', { name: /hello, ada/i })).toBeInTheDocument();
});

Prefer screen over destructuring render's return; toBeInTheDocument needs @testing-library/jest-dom registered in setup.

Click and type like a real useruser-interaction

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

test('submits the form', async () => {
  const user = userEvent.setup();
  render(<Login onSubmit={onSubmit} />);
  await user.type(screen.getByLabelText(/email/i), 'a@b.co');
  await user.click(screen.getByRole('button', { name: /log in/i }));
  expect(onSubmit).toHaveBeenCalled();
});

Use userEvent.setup() and await every call; fireEvent skips real event sequences (focus, keydown, pointer) and hides bugs.

Wait for content that appears after a fetchasync-appearance

render(<Profile userId="1" />);
const name = await screen.findByText(/ada lovelace/i);
expect(name).toBeVisible();

findBy = getBy + waitFor with a 1000ms default timeout; using getBy for async content is the number one cause of 'works locally, fails in CI'.

Assert an element is NOT thereassert-absence

expect(screen.queryByRole('alert')).not.toBeInTheDocument();

// wait for a spinner to go away
await waitForElementToBeRemoved(() => screen.queryByText(/loading/i));

getBy throws when nothing matches, so absence checks must use queryBy.

Wrap renders with app providerscustom-render-providers

// test-utils.tsx
const AllProviders = ({ children }) => (
  <QueryClientProvider client={testClient}>
    <MemoryRouter>{children}</MemoryRouter>
  </QueryClientProvider>
);

export const renderWithProviders = (ui, options) =>
  render(ui, { wrapper: AllProviders, ...options });

Create a fresh QueryClient (or store) per test, or state leaks between tests in ways that only fail when run together.

Pick the right queryquery-priority

// in order of preference:
screen.getByRole('button', { name: /save/i }); // best
screen.getByLabelText(/email/i);   // form fields
screen.getByPlaceholderText(/search/i);
screen.getByText(/no results/i);   // non-interactive
screen.getByTestId('row-42');      // last resort

If getByRole cannot find your element, the markup is usually the bug: a div with onClick has no role.

Test a prop change on the same instancererender-props

const { rerender } = render(<Counter value={1} />);
expect(screen.getByText('1')).toBeInTheDocument();

rerender(<Counter value={2} />);
expect(screen.getByText('2')).toBeInTheDocument();

rerender keeps component state; a second render() call would mount a fresh tree instead.

Test a custom hook directlytest-hooks

import { renderHook, act } from '@testing-library/react';

test('useCounter increments', () => {
  const { result } = renderHook(() => useCounter());
  act(() => result.current.increment());
  expect(result.current.count).toBe(1);
});

renderHook moved into @testing-library/react in v13; the separate react-hooks package is dead, do not install it.

Wait for an assertion to passwaitfor-assertion

await waitFor(() => {
  expect(mockApi.save).toHaveBeenCalledTimes(1);
});

Keep exactly one assertion inside waitFor and never put side effects (clicks, renders) in the callback; it retries the whole block.

See what actually rendereddebug-output

render(<App />);
screen.debug();                 // prints the DOM
screen.debug(undefined, 30000); // raise the truncation limit
logRoles(container);            // prints every ARIA role present

When getByRole fails, logRoles shows you the roles that exist; the error message also suggests close matches.

Query inside one section of the pagescoped-queries

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

const row = screen.getByRole('row', { name: /ada/i });
await user.click(within(row).getByRole('button', { name: /delete/i }));

within() is the clean fix when getByRole matches multiple elements across the page.

Alternatives

PackageRegistryPick it when
@playwright/testnpmYou want component or end-to-end tests in a real browser instead of jsdom
cypressnpmYour team prefers an interactive browser test runner with time-travel debugging
enzymenpmOnly for maintaining legacy test suites; it is unmaintained and has no official adapter beyond React 16