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.
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:
getByRole— withname, this is the right answer for almost everything interactive.getByLabelText— form fields.getByPlaceholderText— only when there is no label, which is itself a bug.getByText— non-interactive content.getByDisplayValue— a filled-in field.getByAltText,getByTitle— images and the rare title.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
// 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:
// 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:
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
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 ↗- TypeScript
typescript/src/web-frameworks/testing-library
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- DOM TestingTesting the layer every front-end framework produces — queries by role and accessible name, event simulation, jsdom's limits, and why selector choice decides maintainability.
- Testing ReactComponents, hooks, context, server components and async state — what to test in a React application, what to leave alone, and how to avoid act warnings.
- Testing Vue.jsVue Test Utils and Testing Library, nextTick and reactivity timing, Pinia stores, composables and the mount-versus-shallowMount decision.
- Testing AngularTestBed, the change detection model that confuses everyone, HttpTestingController, signals and standalone components — testing Angular without fighting it.
- JestThe default JavaScript test runner — configuration, projects, snapshot testing, fake timers, and the choices that keep a large Jest suite fast.
- Accessibility TestingWhat axe and similar tools genuinely catch, the roughly two-thirds they cannot, and how accessible markup makes your test suite more maintainable at the same time.
- CypressIn-browser test execution, automatic retry-ability and time-travel debugging — what Cypress's architecture buys, and the constraints that come with it.
- PlaywrightThe default browser automation tool in 2026 — auto-waiting locators, tracing, sharding and fixtures — with the configuration that matters and the mistakes that still cause flakiness.