skip to content

React Testing Library

You will learn React Testing Library's core premise — drive the component the way a user would and assert on what the DOM exposes. It is the most asked frontend testing library in interviews, usually arriving as 'why getByRole over getByTestId?' and 'why is my test warning about act()?'

on this pageshow

explore

questions

25

A React component renders a user's name only after a fetch promise resolves. In a React Testing Library test, which query do you use to assert that the name appeared, and what must you do with the value that query returns?

level: juniorimportance: must knowfreq 78%

answer

  1. the query family that returns a promise
  2. retries until the DOM matches
  3. one second is the default budget
  4. forget await and you assert on a Promise

basics

~20 s

Use an async findBy* query, such as screen.findByText('Ada Lovelace'), and await it. It returns a promise that retries the query until the element appears or a default 1000 ms timeout expires; unawaited, you assert on a promise instead of the DOM.

solid answer

~50 s

For anything that appears after a promise resolves, reach for the `findBy*` family — `await screen.findByText('Ada Lovelace')`. A `findBy*` query is the synchronous query plus a retry loop: it re-runs the query whenever the DOM mutates and on a short interval, resolving with the element as soon as it matches, and rejecting with a useful "unable to find" error plus a DOM dump if nothing matches within the default one-second timeout. The critical part is `await`. Without it you hold a pending promise, so `expect(...).toBeInTheDocument()` runs against a Promise object rather than an element, and the state update that eventually resolves lands after your test finished — which is where the "not wrapped in act" warnings and stray unhandled rejections come from. `findAllBy*` behaves the same but resolves with an array as soon as at least one match exists.

code

javascript · 7 lines
javascript
import { render, screen } from '@testing-library/react'
import UserCard from './UserCard'

test('renders the fetched name', async () => {
  render(<UserCard userId="1" />)
  expect(await screen.findByText('Ada Lovelace')).toBeInTheDocument()
})

go deeper

for a junior

Know that findBy* queries return promises and must be awaited, and that they are the right tool whenever content appears after a fetch. Never reach for a manual sleep before a synchronous query.

for a middle

Be ready to explain that findBy* is the synchronous query wrapped in a retry loop driven by a MutationObserver and an interval, with a 1000 ms default deadline, and to show the three-argument form that overrides that timeout.

for a senior

Show the diagnosis: an unawaited async query is why act warnings and unhandled rejections appear in a test that is not the one at fault. Explain how you would find and fix those across an existing suite.

for a principal

Own the standard: lint rules that make unawaited queries a build failure, a default timeout the team does not casually raise, and the position that repeated timeout bumps are a determinism problem disguised as a configuration problem.

## The problem: the DOM at t=0 is not the DOM you are asserting about A component that fetches on mount renders twice, at minimum: once immediately (empty, skeleton, or loading state) and again after the promise resolves and React re-renders. `render()` returns after the first of those. Any assertion written on the next line runs against the *first* DOM, which does not yet contain the fetched name. The test does not need a longer sleep — it needs a query that keeps looking. ## What findBy* actually is Every async query in Testing Library is defined as the synchronous query wrapped in the `waitFor` helper. `findByText(...)` is roughly `waitFor(() => getByText(...))`. That gives it three properties worth stating explicitly: - **It returns a promise.** Nothing in it is synchronous. - **It retries.** The underlying `waitFor` observes the container with a `MutationObserver` and also polls on an interval (50 ms by default), re-running the query each time something could have changed. - **It has a deadline.** The default is 1000 ms, exposed as the `asyncUtilTimeout` configuration value. On timeout it rejects with the same readable error a `getBy*` failure produces, including a printout of the DOM it searched, which is usually enough to see that you were waiting on the wrong text. ```js expect(await screen.findByText('Ada Lovelace')).toBeInTheDocument() ``` Because the query itself throws a descriptive error on failure, the `expect` around it is close to redundant — many teams write `await screen.findByText('Ada Lovelace')` on its own line and treat the absence of a rejection as the assertion. ## Why the await is not optional Dropping the `await` breaks the test in two directions at once. Forward: `expect(somePromise).toBeInTheDocument()` is asserting on a promise object, and depending on your matcher set it either fails with a confusing message or, worse, silently passes a truthiness-shaped check. Backward: the retry loop is still running after the test function returns. When the fetch resolves and React sets state, it does so in a component belonging to a test that has already finished — React emits the "An update to X inside a test was not wrapped in act(...)" warning, the failure surfaces in a *different* test's output, and the suite becomes hard to read. Linting helps here; the `await-async-queries` rule in eslint-plugin-testing-library exists precisely because this mistake is invisible when the test happens to pass. ## Passing options The async queries take the query options as the second argument and the wait options as the third, so a slower path is expressed as: ```js await screen.findByText('Ada Lovelace', {}, { timeout: 3000 }) ``` The empty object in the middle is the query options slot. You can also move the default globally with `configure({ asyncUtilTimeout: 3000 })`, but a global bump slows every failing test in the suite by the same factor, so a local option with a comment is usually the better trade. ## findAllBy* and the "at least one" rule `findAllBy*` resolves with an **array** and is satisfied as soon as one element matches. If the component streams in rows, it can resolve when three of the expected ten have rendered. When the count matters, wait for something that only exists in the final state — a total label, an enabled button — rather than asserting the array length straight out of `findAllBy*`. ## What findBy* does not do It does not wait for the network, a timer, or a promise; it only watches the DOM. If the component never renders the text — wrong prop, mock not wired, error branch taken — you get a one-second delay and then a failure, which is the correct outcome. And it is not a substitute for making the test deterministic: an arbitrary `setTimeout` sleep before a synchronous `getBy*` is the anti-pattern that `findBy*` replaces, because a sleep is simultaneously too long on a fast machine and too short on a loaded CI runner.

  • What actually goes wrong if you forget the await on a findBy query?
    Two things. The assertion runs against a pending promise rather than an element, so it proves nothing. And the retry loop plus the component's state update outlive the test, producing an "update not wrapped in act" warning or an unhandled rejection that surfaces in an unrelated test. The eslint rule `await-async-queries` catches it mechanically.
  • How would you wait longer for one query you know is slow?
    Pass wait options as the third argument: `await screen.findByText('Report ready', {}, { timeout: 5000 })`. Prefer that over `configure({ asyncUtilTimeout })`, which raises the deadline for every test and multiplies the cost of every genuine failure. And first ask why it is slow — an unmocked request or a real timer is a determinism bug, not a timeout bug.
  • If several rows appear one by one, is findAllByRole('row') a safe way to assert there are ten?
    No. `findAllBy*` resolves as soon as at least one element matches, so it can return three rows mid-stream and the length assertion fails intermittently. Wait for a marker that exists only in the settled state — a "10 results" label, an enabled Next button — and then read the rows synchronously.

saying these in an interview costs you the question

  • Adding setTimeout or a sleep before a getBy assertion
  • Calling findByText without awaiting the returned promise
  • Thinking findBy re-renders or re-invokes the component
  • Believing findBy waits on the network rather than the DOM
  • Asserting findAllBy length while items still stream in

context

open as a page

A React component test suite queries almost every element with screen.getByTestId(...). Why does React Testing Library treat test ids as a last resort, and what does a test-id-only suite fail to catch?

level: juniorimportance: must knowfreq 70%

basics

~20 s

Test ids are invisible to users, so a test-id-only suite proves markup exists rather than that the interface works. Querying by role, label or visible text makes the query itself verify the text and accessible name a real user relies on.

open as a page

React Testing Library documents a recommended priority order for its queries. What is that order, and why does getByRole sit at the top while getByTestId sits at the bottom?

level: juniorimportance: must knowfreq 80%

basics

~10 s

Queries are ranked by how closely they mirror how a real person finds a control: getByRole with an accessible name first, then getByLabelText, getByPlaceholderText, getByText and getByDisplayValue, then getByAltText and getByTitle, and getByTestId last.

open as a page

In Testing Library, what is the difference between fireEvent.click(button) and userEvent.click(button), and why is user-event the recommended default?

level: juniorimportance: must knowfreq 76%

basics

~20 s

fireEvent dispatches one synthetic DOM event. user-event replays the whole sequence a real interaction produces — pointer, mouse, focus and click events — and refuses to interact with elements a user could not touch, so it catches bugs fireEvent silently passes.

open as a page

A React Testing Library test logs "An update to Profile inside a test was not wrapped in act(...)". What is that warning actually reporting, and what in the test usually causes it?

level: middleimportance: must knowfreq 68%

basics

~20 s

The warning reports that a component's state updated while React was outside an act() scope — almost always a promise resolving after the test's synchronous body finished. React Testing Library already wraps render, events and its async utilities in act, so the real fix is awaiting the UI change the promise causes.

open as a page

In a React Testing Library test, when do you need waitFor instead of an async findBy* query, and what should never go inside the waitFor callback?

level: middleimportance: must knowfreq 62%

basics

~20 s

Reach for waitFor only when the thing you are waiting on is not "an element appeared" — a mock being called, an attribute flipping, text changing. Its callback re-runs on every DOM mutation and interval, so it must contain assertions only, never clicks, requests or other side effects.

open as a page

After clicking "Add to cart" in a React Testing Library test, a developer asserts expect(useCartStore.getState().items).toHaveLength(1) on the Zustand store instead of asserting on what the page renders. What is wrong with that assertion, and when is asserting on state acceptable?

level: middleimportance: must knowfreq 55%

basics

~20 s

Asserting on store state proves the plumbing ran, not that the user saw anything: the store can update while the component fails to render the change. Assert the rendered outcome instead, and the state assertion becomes redundant.

open as a page

Testing Library exposes each query as getBy*, queryBy* and findBy* (plus the All variants). What does each return or throw, and which one do you use to assert that an element is NOT on the page?

level: middleimportance: must knowfreq 70%

basics

~20 s

getBy* returns the single match and throws if there is none (or more than one); queryBy* returns null instead of throwing; findBy* returns a promise that retries. Assert absence with queryBy*, because getBy* throws before your matcher can run.

open as a page

A React component under test needs a router, a theme provider and a data-fetching client just to mount. How do you supply that context in a React Testing Library test, and why do teams put it behind a custom render exported from a shared test-utils module?

level: middleimportance: must knowfreq 70%

basics

~20 s

Pass the provider stack as render's wrapper option — a component that receives the UI as children — then hide that behind a custom render exported from a shared test-utils module which re-exports the rest of the library. Provider changes become one edit instead of hundreds.

open as a page

In React Testing Library, where does render() actually put the component, and what does the library's automatic cleanup do between tests?

level: juniorimportance: should knowfreq 58%

basics

~20 s

render() mounts the component into a fresh div appended to document.body, so tests query real DOM. Automatic cleanup unmounts it after each test, so every test starts from an empty document instead of inheriting the previous test's markup.

open as a page

A React Testing Library test renders a table where every row has a Delete button, and screen.getByRole('button', { name: /delete/i }) fails with "found multiple elements". How do you target the button in one specific row without dropping to a CSS selector?

level: middleimportance: should knowfreq 55%

basics

~20 s

Scope the query instead of widening it: find the row with an accessible query, pass that element to within(), then run the role query inside it — within(row).getByRole('button', { name: /delete/i }). Indexing getAllBy results is the fragile fallback.

open as a page

A React Testing Library test fails with "unable to find an element with the text: Hello Ada", although the page renders that phrase as <span>Hello </span><span>Ada</span>. Why does getByText miss it, and what matching options does the query give you?

level: middleimportance: should knowfreq 45%

basics

~20 s

By default the text matcher compares against each element's own text nodes, not the combined text of its descendants, so a phrase split across child elements matches no single element. Options are a regex, exact: false, a custom normalizer, or a function matcher.

open as a page

A React Testing Library test fails with "Unable to find an element…" and there is no browser to look at. What does the library give you to inspect what was actually rendered, and how do you keep that output usable on a large tree?

level: middleimportance: should knowfreq 42%

basics

~20 s

Print the rendered DOM: screen.debug() formats the current document body, prettyDOM formats any node you hand it, and logRoles lists the roles in a container. Scope the output to one element and raise the print limit when it truncates.

open as a page

You need a React Testing Library test to assert how a component behaves after one of its props changes. Why is calling render() a second time the wrong way to do that, and what do the rerender and unmount functions returned by render give you instead?

level: middleimportance: should knowfreq 52%

basics

~20 s

A second render() mounts a second copy in its own container, so both exist in the document and queries match twice. rerender() re-renders into the same container so the component updates instead of remounting, and unmount() tears it down so you can assert teardown.

open as a page

Why does Testing Library's user-event require a userEvent.setup() call and return a promise from every interaction, instead of exposing plain synchronous helpers like fireEvent does?

level: middleimportance: should knowfreq 50%

basics

~20 s

Because one interaction is many ordered events with optional delays between them, which needs an asynchronous API, and because interactions share state — held modifier keys, pressed mouse buttons, the clipboard. setup() creates the instance that carries that state and your configuration.

open as a page

A test fills a search box with Testing Library's fireEvent.change(input, { target: { value: 'shoes' } }) and the assertion passes. Which real-user behaviours does that single line fail to exercise?

level: middleimportance: should knowfreq 55%

basics

~20 s

That line jumps the value from empty to 'shoes' in one synthetic change event. It produces no key events and no per-character input events, so keyboard handlers, per-keystroke logic, maxlength enforcement, caret-position formatting and focus behaviour all go untested.

open as a page

A React component debounces its search input with setTimeout. A test calls jest.useFakeTimers(), and now `await userEvent.type(input, 'ada')` never resolves in a React Testing Library test. Why does it hang, and how do you make user-event and RTL's async helpers work under fake timers?

level: seniorimportance: should knowfreq 42%

basics

~20 s

user-event schedules delays between keystrokes with the timer API, so once fake timers are installed nothing advances the clock and its promise never settles. Pass an advance function at setup — userEvent.setup({ advanceTimers: jest.advanceTimersByTime }) — so user-event drives the fake clock itself.

open as a page

A React Testing Library test clicks Save and then asserts `expect(screen.queryByText('Saving…')).not.toBeInTheDocument()`. Why can that assertion pass even when the spinner logic is broken, and what does waitForElementToBeRemoved give you instead?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The assertion runs before the spinner has even mounted, so it passes by asserting the absence of something that was never present — a vacuous green. waitForElementToBeRemoved throws if the element is missing at call time, forcing the test to prove it appeared and then went away.

open as a page

A React Testing Library test for a Checkout page mocks out every child component (jest.mock('./CartSummary') and friends) so only the page's own markup renders. What does that buy, what does it stop catching, and when is stubbing a child justified?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Stubbing every child makes the test fast and isolated, but it removes the rendered UI the test was supposed to check, leaving only assertions about props passed to stubs. Stub a child only when it is genuinely untestable in that environment.

open as a page

In a React Testing Library test, screen.getByRole('button', { name: 'Save' }) throws even though the Save control is visibly rendered and clickable in the browser. What markup causes that, and why is the failing query often a real defect rather than a test problem?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Role queries read the accessibility tree, so they miss a control that has no role (a clickable div), is hidden from that tree (aria-hidden, display:none, the hidden attribute), or whose accessible name is not the text you passed. Each of those is usually a genuine bug.

open as a page

A React component test suite passes file by file but fails when the whole suite runs, and the failures move around when you change the test order. Every test mounts through a shared helper that wraps components in a store and a cache provider. How do you track that down, and what is the structural fix?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Order-dependent failures mean state is surviving between tests — typically a store or cache created once at module scope and shared by every mount. Confirm it by bisecting the run order, then build those instances inside the render helper so each test gets fresh providers.

open as a page

A Testing Library test clicks a button with fireEvent.click and passes, but in the browser that button does nothing because a CSS rule sets pointer-events: none on it. Why did the test still pass, and what would user-event do differently?

level: seniorimportance: should knowfreq 43%

basics

~20 s

fireEvent calls dispatchEvent directly, skipping everything the browser decides before an event exists — so it clicks elements no user could reach. user-event checks the element's effective pointer-events and throws instead of clicking, turning the silent pass into a failure.

open as a page

If Testing Library's user-event is the default for interactions, when is fireEvent still the right tool in a component test?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Use fireEvent for events no human gesture produces directly — scroll, media events, animationend and transitionend, load and error on an image, or a custom event from a third-party widget — and when you need precise control over an event's properties. Never as a shortcut for a click or keystroke.

open as a page

Component tests written with React Testing Library time out intermittently in CI, and a teammate proposes raising the global asyncUtilTimeout from one second to ten. How would you evaluate that proposal, and what waiting policy would you set for the suite?

level: principalimportance: should knowfreq 24%

basics

~20 s

Timeouts are a symptom, so classify the failures first: genuinely slow but deterministic, nondeterministic, or waiting on something that will never appear. A global bump only helps the first class and makes every real failure ten times slower to report, so keep the default tight and fix determinism instead.

open as a page

Your team's React Testing Library suite is green on every pull request, yet UI regressions keep reaching production. A review shows tests built almost entirely on test ids, mocked-out child components, and assertions against store state. As the lead, how do you decide what to change and in what order?

level: principalimportance: should knowfreq 30%

basics

~20 s

Start by measuring sensitivity, not coverage: break components deliberately and see which tests stay green. Then rewrite the highest-risk flows first, as they are touched, and change the defaults so new tests do not repeat the pattern.

open as a page