You are viewing documentation for v14 alpha. This version is under active development and APIs may change.

LLM Guidelines for React Native Testing Library

Actionable guidelines for writing tests with React Native Testing Library (RNTL) v14.

Core APIs

render

const result = await render(<Component />, options?);
OptionDescription
wrapperReact component to wrap the rendered component (e.g., providers)
createNodeMockFunction to create mock refs
ReturnDescription
rerender(component)Re-render with a new component (async)
unmount()Unmount the rendered component (async)
toJSON()Get JSON representation for snapshots
debug(options?)Print the component tree to console
containerRoot host element of the rendered tree
rootFirst child host element (your component's root)

screen

Prefer screen over destructuring from render(). Provides all query methods after render() is called.

await render(<Component />);
screen.getByRole('button'); // Access queries via screen

renderHook

const { result, rerender, unmount } = await renderHook(() => useMyHook(), options?);
OptionDescription
initialPropsInitial props passed to the hook
wrapperReact component to wrap the hook (e.g., providers)
ReturnDescription
result.currentCurrent return value of the hook
rerender(props?)Re-render hook with new props (async)
unmount()Unmount the hook (async)

Query Selection

  • Prefer getByRole as first choice for querying elements
  • Query priority: getByRolegetByLabelTextgetByPlaceholderTextgetByTextgetByDisplayValuegetByTestId (last resort)
  • Use findBy* for elements that appear asynchronously (after API calls, timeouts, state updates)
  • Use queryBy* ONLY for checking non-existence (with .not.toBeOnTheScreen())
  • Never use getBy* for non-existence checks
  • Avoid container.queryAll() - use screen queries instead
  • Query by visible text, not testID when text is available

Assertions

  • Use RNTL matchers - prefer semantic matchers over prop assertions
  • Combine queries with matchers: expect(screen.getByText('Hello')).toBeOnTheScreen()
  • No redundant null checks - getBy* already throws if not found

Jest Matchers Reference

MatcherDescription
toBeOnTheScreen()Element is present in the element tree
toBeVisible()Element is visible (checks style, aria-hidden, accessibilityElementsHidden, ancestors)
toBeEmptyElement()Element has no children or text content
toContainElement(element)Element contains another element
toBeEnabled()Element is not disabled (checks aria-disabled, accessibilityState, ancestors)
toBeDisabled()Element has aria-disabled or accessibilityState={{ disabled: true }} (checks ancestors)
toBeBusy()Element has aria-busy or accessibilityState={{ busy: true }}
toBeChecked()Element has aria-checked or accessibilityState={{ checked: true }}
toBePartiallyChecked()Element has aria-checked="mixed" or accessibilityState={{ checked: 'mixed' }}
toBeSelected()Element has aria-selected or accessibilityState={{ selected: true }}
toBeExpanded()Element has aria-expanded or accessibilityState={{ expanded: true }}
toBeCollapsed()Element has aria-expanded={false} or accessibilityState={{ expanded: false }}
toHaveTextContent(text)Element has matching text content
toHaveDisplayValue(value)TextInput has matching display value
toHaveAccessibleName(name?)Element has matching aria-label, accessibilityLabel, or text content
toHaveAccessibilityValue(value)Element has matching aria-value* or accessibilityValue
toHaveStyle(style)Element has matching style
toHaveProp(name, value?)Element has prop (use semantic matchers when possible)

User Interactions

Prefer userEvent over fireEvent for realistic user interaction simulation. userEvent triggers the complete event sequence that real users would produce.

userEvent (Preferred)

const user = userEvent.setup();
MethodDescription
user.press(element)Press an element (triggers pressIn, pressOut, press)
user.longPress(element, options?)Long press with optional { duration }
user.type(element, text, options?)Type into TextInput (triggers focus, keyPress, change, changeText per char)
user.clear(element)Clear TextInput (select all + backspace)
user.paste(element, text)Paste text into TextInput
user.scrollTo(element, options)Scroll a ScrollView with { y } or { x } offset

fireEvent (Low-level)

Use only when userEvent doesn't support the event or when you need direct control.

MethodDescription
fireEvent(element, eventName, ...data)Fire any event by name
fireEvent.press(element)Fire onPress only (no pressIn/pressOut)
fireEvent.changeText(element, text)Fire onChangeText directly
fireEvent.scroll(element, eventData)Fire onScroll with event data

Async/Await (v14)

  • Always await: render(), fireEvent.*, renderHook(), userEvent.*
  • Make test functions async: test('name', async () => { ... })
  • Don't wrap in act() - render and fireEvent handle it internally

waitFor Usage

  • Use findBy* instead of waitFor + getBy* when waiting for elements
  • Never perform side-effects (like fireEvent.press()) inside waitFor callbacks
  • One assertion per waitFor callback
  • Never pass empty callbacks - always include a meaningful assertion
  • Place side-effects before waitFor - perform actions, then wait for result

Code Organization

  • Use screen instead of destructuring from render(): screen.getByText() not const { getByText } = render()
  • Prefer userEvent over fireEvent for realistic interactions
  • Don't use cleanup() - handled automatically
  • Name wrappers descriptively: ThemeProvider not Wrapper
  • Install ESLint plugin: eslint-plugin-testing-library

Quick Checklist

  • ✅ Using getByRole as first choice?
  • ✅ Using await for all async operations?
  • ✅ Using findBy* for async elements (not waitFor + getBy*)?
  • ✅ Using queryBy* only for non-existence?
  • ✅ Using RNTL matchers (toBeOnTheScreen(), toBeDisabled(), etc.)?
  • ✅ Using screen not destructuring from render()?
  • ✅ Avoiding side-effects in waitFor?
  • ✅ Using userEvent when appropriate?

Example: Good Pattern

import { render, screen } from '@testing-library/react-native';
import userEvent from '@testing-library/react-native';
import { Pressable, Text, TextInput, View } from 'react-native';

test('user can submit form', async () => {
  const user = userEvent.setup();

  const Component = () => {
    const [name, setName] = React.useState('');
    const [submitted, setSubmitted] = React.useState(false);

    return (
      <View>
        <TextInput role="textbox" aria-label="Name" value={name} onChangeText={setName} />
        <Pressable role="button" aria-label="Submit" onPress={() => setSubmitted(true)}>
          <Text>Submit</Text>
        </Pressable>
        {submitted && <Text role="alert">Form submitted!</Text>}
      </View>
    );
  };

  await render(<Component />);

  // ✅ getByRole as first choice
  const input = screen.getByRole('textbox', { name: 'Name' });
  const button = screen.getByRole('button', { name: 'Submit' });

  // ✅ userEvent for realistic interactions
  await user.type(input, 'John Doe');
  await user.press(button);

  // ✅ findBy* for async elements
  const successMessage = await screen.findByRole('alert');

  // ✅ RNTL matchers
  expect(successMessage).toBeOnTheScreen();
  expect(successMessage).toHaveTextContent('Form submitted!');
});

Example: Anti-Patterns

// ❌ Missing await
test('bad', () => {
  render(<Component />);
  fireEvent.press(screen.getByText('Submit'));
});

// ❌ getBy* for non-existence
expect(screen.getByText('Error')).not.toBeOnTheScreen();

// ❌ waitFor + getBy* instead of findBy*
await waitFor(() => {
  expect(screen.getByText('Loaded')).toBeOnTheScreen();
});

// ❌ Side-effect in waitFor
await waitFor(async () => {
  await fireEvent.press(button);
  expect(screen.getByText('Result')).toBeOnTheScreen();
});

// ❌ accessibility* props instead of ARIA
<Pressable accessibilityRole="button" accessibilityLabel="Submit" />;

// ❌ Destructuring from render
const { getByText } = await render(<Component />);

By following these guidelines, your tests will be more maintainable, accessible, and reliable.

Need React or React Native expertise you can count on?