Skip to content
End To End Tester

Testing Library

The query API shared across React, Vue, Angular and Svelte — its guiding principle, the queries, user-event, and the mistakes it is designed to prevent.

2 min read · updated 19 September 2026

Testing Library is a set of queries for finding elements in rendered output, plus thin per-framework bindings that render a component and clean up afterwards. The query API is the product; the bindings are a detail.

The more your tests resemble the way your software is used, the more confidence they can give you.

Everything else follows from that sentence.

#The queries

Three prefixes, six suffixes, one table worth memorising:

No match Match Multiple Async
getBy… throws element throws no
queryBy… null element throws no
findBy… rejects element rejects yes
getAllBy… throws array array no
queryAllBy… [] array array no
findAllBy… rejects array array yes

getBy when it should be there now. queryBy only to assert absence. findBy when it appears after an await.

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

test('shows the total once a quantity is entered', async () => {
  const user = userEvent.setup();
  render(<BasketLine sku="book-1" unitCents={1_200} />);

  await user.clear(screen.getByRole('spinbutton', { name: 'Quantity' }));
  await user.type(screen.getByRole('spinbutton', { name: 'Quantity' }), '3');

  expect(await screen.findByText('£36.00')).toBeVisible();
  expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});

#The query priority

The library documents an explicit order and it is worth following:

  1. getByRole — with name, this is the right answer for almost everything interactive.
  2. getByLabelText — form fields.
  3. getByPlaceholderText — only when there is no label, which is itself a bug.
  4. getByText — non-interactive content.
  5. getByDisplayValue — a filled-in field.
  6. getByAltText, getByTitle — images and the rare title.
  7. getByTestId — escape hatch.

Reaching for getByTestId is a signal. It usually means the element has no accessible role or name, which means a screen-reader user cannot find it either. Sometimes the right fix is a test id; more often it is an aria-label.

#user-event, not fireEvent

typescript
// fireEvent dispatches exactly one event.
fireEvent.click(button);

// userEvent dispatches the sequence a real click produces, respects
// disabled elements, moves focus, and handles pointer capture.
await userEvent.setup().click(button);

The difference matters. A component that listens for pointerdown rather than click passes a fireEvent.click test and fails for real users. userEvent is the default; fireEvent is for the occasional case where you genuinely need one specific event.

Remember setup() — calling userEvent.click directly still works but skips the document setup and advances fake timers differently.

#Custom render

Nearly every project wants a wrapper that supplies providers:

typescript
// test-utils.tsx — import this instead of the library everywhere
import { render, type RenderOptions } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

function AllProviders({ children }: { children: React.ReactNode }) {
  const client = new QueryClient({
    defaultOptions: { queries: { retry: false } }   // no retries in tests
  });
  return (
    <QueryClientProvider client={client}>
      <ThemeProvider theme={testTheme}>{children}</ThemeProvider>
    </QueryClientProvider>
  );
}

const customRender = (ui: React.ReactElement, options?: RenderOptions) =>
  render(ui, { wrapper: AllProviders, ...options });

export * from '@testing-library/react';
export { customRender as render };

A fresh QueryClient per render is important — a shared one carries cache between tests and produces order-dependent failures.

#Matchers

@testing-library/jest-dom adds the assertions that make failures readable:

typescript
expect(button).toBeDisabled();
expect(input).toHaveValue('4242');
expect(alert).toHaveTextContent(/declined/i);
expect(dialog).toBeVisible();
expect(field).toBeRequired();
expect(link).toHaveAccessibleName('Read the full article');
expect(form).toHaveFormValues({ quantity: 3, gift: true });

#Debugging

typescript
screen.debug();                            // print the DOM
screen.debug(screen.getByRole('form'));    // print one subtree
screen.logTestingPlaygroundURL();          // open it in Testing Playground

// The best one: when a query fails, the error already lists every role
// present in the document, which usually makes the fix obvious.

#What it deliberately will not do

There is no API for reading component state, calling instance methods, inspecting props or shallow-rendering. That is a design decision, not an omission: each of those lets a test pass while the screen is wrong.

The complaint people have — "I cannot test this private behaviour" — is usually the library working. If a behaviour has no observable effect, a user cannot experience it, and a test asserting it is asserting an implementation detail. See writing testable code.

#Where it stops

jsdom has no layout, so anything visual is out of reach — see DOM testing. For those assertions use a real browser: Playwright or Cypress component testing, or Vitest browser mode. The queries are the same in all of them, which is the nicest thing about having learned them once.

Common questions

What is the guiding principle of Testing Library?
'The more your tests resemble the way your software is used, the more confidence they can give you.' Everything in the API follows from it — queries that mirror how a user finds things, events that mirror how a user produces them, and the deliberate absence of any way to inspect component internals.
Why does Testing Library not let me access component state?
Because a test that asserts on state passes when the state is right and the screen is wrong. Restricting assertions to rendered output means the test can only fail for reasons a user would notice, which is the entire point.
Is it slower than shallow rendering?
Somewhat, because children actually render. It is also far more likely to catch a real defect, and shallow rendering has largely fallen out of favour for exactly that reason.

Runnable samples for this page

last test results ↗
  • TypeScripttypescript/src/web-frameworks/testing-library

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?