skip to content

React Native Testing Library

React Native Testing Library renders components in Jest and queries them the way a user finds them, by role, label and text. Interviewers probe whether you test behaviour rather than internals.

on this pageshow

explore

questions

20

In React Native Testing Library 14, why can getByText still miss a list loaded by a mocked fetch after await render(), and what should the test use instead?

level: juniorimportance: must knowfreq 66%

answer

  1. render finishes React work, not your promise
  2. getBy is one synchronous check
  3. findBy is getBy plus waitFor
  4. retries every 50 ms for 1000 ms
  5. await the findBy, then plain getBy

basics

~20 s

Awaiting RNTL 14's render() completes the mount but does not wait for a mocked fetch to resolve, and getByText checks the tree once. Use await screen.findByText(), which retries every 50 ms until the item appears or 1000 ms pass.

solid answer

~40 s

In RNTL 14 `render` is async: awaiting it lets React finish the mount and flush the updates already queued, which is what makes Suspense and `use()` testable. It knows nothing about the `fetchBooks` promise your effect started, so whether the titles are in the tree on the next line is a matter of timing, not something the test guarantees. `getByText` is one synchronous check that throws when nothing matches. The fix is `expect(await screen.findByText('Dune')).toBeOnTheScreen()`: a `findBy*` query is a `getBy*` wrapped in `waitFor`, re-run every 50 ms until it matches or 1000 ms pass, and its promise has to be awaited. When the whole list arrives in one state update, the assertions after that first `findBy` can use plain `getBy*`.

code

typescript · 17 lines
typescript
import { render, screen } from '@testing-library/react-native';
import { CatalogScreen } from './CatalogScreen';
import { fetchBooks } from './api';

jest.mock('./api');
const mockFetchBooks = jest.mocked(fetchBooks);

test('shows the loaded catalog', async () => {
  mockFetchBooks.mockResolvedValue([
    { id: 'b1', title: 'Dune' },
    { id: 'b2', title: 'Emma' },
  ]);
  await render(<CatalogScreen />);

  expect(await screen.findByText('Dune')).toBeOnTheScreen();
  expect(screen.getByText('Emma')).toBeOnTheScreen();
});

go deeper

for a junior

Recall that getBy checks once, findBy waits, and that both render and findBy must be awaited in RNTL 14. Know the 1000 ms default timeout.

for a middle

Explain that findBy is getBy run through waitFor at a 50 ms interval, and why await render() flushes React's queued work but not the component's own promises.

for a senior

Show how you keep async tests deterministic: mock at the network boundary, wait on the first user-visible consequence, and assert the rest synchronously so failures point at the real async step.

for a principal

Frame the team convention: lint rules for awaited async utilities, one shared data-mocking layer, and a rule that no test sleeps, so async flakiness is prevented by construction.

## The failing test A library-catalog screen, `CatalogScreen`, calls `fetchBooks()` inside `useEffect`, stores the result with `setBooks`, and renders the titles in a `FlatList`. The test mocks the API module so `fetchBooks` resolves with two books, awaits `render(<CatalogScreen />)`, and asserts `screen.getByText('Dune')`. Sometimes it passes, sometimes it throws "Unable to find an element with text: Dune", and it fails every time once the mock is given a small delay. ## What `await render()` does in RNTL 14 **React Native Testing Library (RNTL)** 14 made `render` asynchronous. It returns a Promise because it runs the mount inside React 19's async `act`, which lets React finish rendering, run the effects of that commit and flush the updates that are already queued before your next line runs. That is what makes components that suspend, or that call `use()`, testable. What `await render()` does **not** do is wait for work your component started and React knows nothing about: - the `fetchBooks()` promise created in the effect; - the `res.json()` call chained on it; - a `setTimeout`, a debounce or an animation the screen schedules; - anything that finishes after an unknown number of microtask or timer turns. So the state update that puts the books in the tree may or may not have happened by the time the next line executes. A test that passes today because the mock resolves within the turns React happened to flush is relying on an implementation detail of the mock. This is a common misreading after a v13 to v14 upgrade. In v13 `render` was synchronous, and teams that ran the `rntl-v14-async-functions` codemod now see `await render(...)` everywhere and assume the `await` covers data loading too. It covers React's own work, which is why a component that suspends on a promise passed to `use()` is handled, but a plain `useEffect` fetch followed by `setState` is ordinary application code that React does not track. ## The query families and which one waits | Query | Returns | When nothing matches | Waits? | |---|---|---|---| | `getBy*` | the element | throws at once | no | | `queryBy*` | the element or `null` | returns `null` | no | | `findBy*` | a Promise of the element | rejects after the timeout | yes | `getByText` is a **single synchronous check** of the rendered tree. `findByText` is that same check run through `waitFor`: RNTL calls the `getBy` query, and while it throws, calls it again every **50 ms** (`interval`) until it succeeds or **1000 ms** (`timeout`) have passed. On success the promise resolves with the element; on timeout it rejects with the last "Unable to find" error, with the rendered element tree appended so you can see what was on screen. ## Writing the catalog test 1. Mock the network boundary so the test controls the data. 2. Await `render`. 3. Await the first thing that depends on the async work with a `findBy*` query. 4. Assert the rest synchronously, since it arrived in the same update. ```tsx test('shows the loaded catalog', async () => { mockFetchBooks.mockResolvedValue([ { id: 'b1', title: 'Dune' }, { id: 'b2', title: 'Emma' }, ]); await render(<CatalogScreen />); expect(await screen.findByText('Dune')).toBeOnTheScreen(); expect(screen.getByText('Emma')).toBeOnTheScreen(); }); ``` The second assertion needs no waiting because `setBooks` put both titles in the tree in one update: once `Dune` is visible, `Emma` is too. If the screen loaded data in two separate steps, the second step would need its own `findBy*`. ## Mistakes interviewers listen for - **Believing `await render()` waits for effects' network calls.** It flushes React work, not your promises. - **Dropping the `await` on `findBy*`.** The query then returns a pending promise, `expect` receives a Promise instead of an element, and the test can end before the wait settles. - **Padding with sleeps.** A `setTimeout`-based pause or an empty `await act(async () => {})` encodes a guess about how many turns the mock needs; it breaks as soon as the mock or the fetch chain changes. - **Using `findBy*` for every later assertion.** It is harmless, but it hides which step was actually asynchronous and slows failures down to the full timeout. - **Reaching for `waitFor(() => screen.getByText(...))`.** It works, but `findBy*` is the purpose-built form and produces a better error. The general rule: `await render()` for the mount, `findBy*` for the first thing that depends on asynchronous work, plain `getBy*` or `queryBy*` afterwards.

  • What goes wrong if the test calls screen.findByText('Dune') without await?
    The query returns a pending promise, so `expect` receives a Promise rather than an element and the assertion is meaningless. The test body can also finish while the wait is still polling; RNTL's cleanup then unmounts the screen and aborts the pending wait, and the resulting error points at an un-awaited `findBy*` or `waitFor`. The `await-async-utils` rule of eslint-plugin-testing-library catches the related `waitFor` mistake at lint time.
  • Why not add `await act(async () => {})` after render and keep getByText?
    An empty async `act` drains whatever is queued at that moment. Whether the mocked fetch, its `json()` step and the state update all complete inside that window depends on how the mock is written, so the test encodes a guess about timing. A `findBy*` query states the actual condition, the title being on screen, and keeps working when the fetch chain or the mock changes.

saying these in an interview costs you the question

  • Awaiting render() in RNTL 14 waits for every effect's network call to finish.
  • getByText keeps retrying until the element appears.
  • findByText works fine without await because it is still a query.
  • Add a setTimeout pause after render so the fetch has time to finish.
  • Every assertion after the first findBy must also use findBy.
open as a page

With React Native Testing Library, what is the difference between toBeOnTheScreen and toBeVisible, and which one proves a survey's error message is actually shown?

level: juniorimportance: must knowfreq 58%

basics

~20 s

toBeOnTheScreen passes when the element is still attached to the rendered tree; toBeVisible also fails if it or any ancestor has display: 'none', opacity: 0, is hidden from accessibility, or sits in a Modal with visible={false}. Only toBeVisible proves the message is shown.

open as a page

In React Native Testing Library 14, how do you render a component and find an element through screen, and why must render be awaited?

level: juniorimportance: must knowfreq 58%

basics

~10 s

In RNTL 14 you write await render(<LoginScreen />), then query with screen.getByText('Welcome back'). render is async because it wraps rendering in an async act, and screen is only bound after that promise resolves.

open as a page

In React Native Testing Library 14, how do you type a search and press a filter chip with userEvent, and why await each call?

level: juniorimportance: must knowfreq 50%

basics

~20 s

Create an instance with userEvent.setup(), then await user.type(searchInput, 'dune') and await user.press(chip), and assert on the rendered result. Each call returns a Promise that settles after its events and React updates, so skipping await lets assertions run too early.

open as a page

With React Native Testing Library, when do you use waitFor instead of a findBy query, and how do you change their timeout per call and globally?

level: middleimportance: must knowfreq 55%

basics

~20 s

Use findBy* when waiting for an element to appear and waitFor for any other expectation, such as a mock being called. Pass { timeout } per call (findBy's third argument) or set configure({ asyncUtilTimeout }) for the whole suite; the default is 1000 ms.

open as a page

In React Native Testing Library, why can toBeDisabled fail on a survey Submit button that ignores presses, and what state do toBeDisabled and toBeChecked read?

level: middleimportance: must knowfreq 45%

basics

~20 s

toBeDisabled reads the host element's aria-disabled or accessibilityState.disabled, and its ancestors', not whether onPress does anything. A button that just ignores presses is enabled to RNTL. toBeChecked reads a Switch's value, or aria-checked on a checkbox, radio or switch role.

open as a page

In React Native Testing Library, what makes an element findable with getByRole, and how does the name option pick a login form's Sign in button?

level: middleimportance: must knowfreq 48%

basics

~20 s

getByRole matches only accessibility elements (Text, TextInput and Switch by default, or anything with accessible true, like the View a Pressable renders) whose role or accessibilityRole matches. The name option compares the accessible name: the label, else the text content.

open as a page

In React Native Testing Library, how does userEvent.press differ from fireEvent.press, and when is fireEvent still the right tool?

level: middleimportance: must knowfreq 52%

basics

~20 s

fireEvent.press finds the nearest enabled onPress handler, walking up through composite components, and calls it once. userEvent.press replays React Native's press sequence on host elements, pressIn, pressOut, then press, in at least 130 ms. Keep fireEvent for events userEvent lacks.

open as a page

In React Native Testing Library, what does waitForElementToBeRemoved require before it waits, and when is waitFor with a queryBy check the better fit?

level: middleimportance: should knowfreq 30%

basics

~20 s

waitForElementToBeRemoved takes a query callback whose element must exist on the first call, or it throws at once, then polls until the query throws or returns null or an empty array. If the element may already be gone, use waitFor with queryBy.

open as a page

In React Native Testing Library 14, how do you give every rendered survey screen its providers, and what does the render wrapper option do that inline wrapping does not?

level: middleimportance: should knowfreq 40%

basics

~10 s

Pass providers through render's wrapper option, a component that receives the tested element as children, or build an async renderWithProviders helper around it. RNTL re-applies the wrapper on every rerender, so providers survive updates.

open as a page

In React Native Testing Library, does toHaveTextContent match the whole text or a substring, and how does toHaveAccessibleName compute what it compares?

level: middleimportance: should knowfreq 32%

basics

~20 s

toHaveTextContent compares the element's full concatenated text, trimmed and whitespace-collapsed, exactly by default; use a RegExp or { exact: false } for a partial match. toHaveAccessibleName compares the computed name: labelledby, then label, then text content.

open as a page

In React Native Testing Library 14, why do queries return host elements like View and Text instead of your own composite components?

level: middleimportance: should knowfreq 28%

basics

~20 s

RNTL 14 renders with Test Renderer, whose tree holds only host elements such as View, Text and TextInput, the ones with a native counterpart. Composite components live only in JavaScript, so queries return what would reach the native view.

open as a page

In React Native Testing Library, how should a test find a login form's email and password TextInputs, given TextInput has no default role?

level: middleimportance: should knowfreq 40%

basics

~10 s

TextInput has no default role, so after *ByRole the RNTL priority puts the text-input queries: getByLabelText first, matching accessibilityLabel, aria-label or a labelled-by nativeID, then getByPlaceholderText and getByDisplayValue. getByTestId stays the last resort.

open as a page

In React Native Testing Library, which events does userEvent.type emit on a TextInput, and how do clear, paste and the type options change that?

level: middleimportance: should knowfreq 30%

basics

~20 s

userEvent.type fires focus, then keyPress, change, changeText and selectionChange per character, then endEditing and blur, appending text and honouring maxLength. clear empties the field, paste replaces it in one change, and skipPress, skipBlur and submitEditing adjust the edges.

open as a page

With React Native Testing Library and jest.useFakeTimers(), how do waitFor and userEvent.setup advance the fake clock, and when must you pass advanceTimers yourself?

level: seniorimportance: should knowfreq 36%

basics

~10 s

RNTL detects Jest fake timers: waitFor and findBy advance the fake clock by interval until the timeout is spent, and userEvent.setup's default advanceTimers calls jest.advanceTimersByTime. Pass advanceTimers only for a non-Jest fake clock.

open as a page

In a React Native Testing Library test, why can pressing a button inside a waitFor callback call the mocked API more than once, and how do you restructure it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

waitFor re-runs its whole callback every 50 ms until it stops throwing, so a press inside it is repeated on each failed check. Press once before waitFor and keep only the assertion inside, or await a findBy query instead.

open as a page

Upgrading a React Native suite from React Native Testing Library 13 to 14, what breaks, and what do the rntl-v14 codemods fix for you?

level: seniorimportance: should knowfreq 38%

basics

~10 s

RNTL 14 needs React 19 and React Native 0.78+, makes render, renderHook, fireEvent and act async, swaps react-test-renderer for test-renderer and removes update, UNSAFE_root, UNSAFE_*ByType/ByProps, concurrentRoot and createNodeMock. Codemods update dependencies and add awaits.

open as a page

A React Native Testing Library query fails although screen.debug() prints the element; which accessibility rules can hide it, and what does includeHiddenElements change?

level: seniorimportance: should knowfreq 22%

basics

~20 s

RNTL queries skip elements hidden from accessibility by default: display none, aria-hidden, accessibilityElementsHidden, importantForAccessibility no-hide-descendants on the element or an ancestor, or a modal sibling. includeHiddenElements: true, or configure's defaultIncludeHiddenElements, makes queries match them anyway.

open as a page

A React Native Testing Library test logs 'not wrapped in act(...)' after pressing a movie filter chip; what causes the warning and how do you fix it?

level: seniorimportance: should knowfreq 32%

basics

~20 s

A React state update ran outside act: usually an un-awaited user.press or fireEvent call, or async work it started, such as a fetch or debounce timer, settling after the test's last await. Await every event, then await the visible outcome.

open as a page

In React Native Testing Library, how do you simulate scrolling a FlatList of movies with userEvent.scrollTo so that more rows render?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

Call await user.scrollTo(list, { y: 300, contentSize, layoutMeasurement }) on the FlatList's host ScrollView. Without contentSize and layoutMeasurement the list has no dimensions in the test, so it cannot compute a new window of rows to render.

open as a page