Testing Components
Render with React Native Testing Library, query by role and text, press with userEvent, and wait for the async update.
await render(<Component />); screen.getByRole('button', { name: 'Save' })A component test renders one component and asserts what a person would see. It is the cheapest test that still covers JSX, state and press handlers working together.
render, screen, query
React Native Testing Library's render mounts the element, and screen holds the queries for whatever was rendered last. Query in the order a user perceives things: getByRole('button', { name: 'Save' }) first, then getByText, then getByLabelText, and only fall back to getByTestId when nothing user-visible identifies the element.
These are not CSS selectors, and there is no DOM to select from. getByRole reads the accessibilityRole prop, and the accessible name comes from accessibilityLabel or from the element's own text. A Pressable with no role set is invisible to a role query.
Events and waiting
userEvent.setup() returns an object whose press and type fire the full sequence a real touch produces, including focus and blur. fireEvent.press is the shorter, lower-level form: it calls the one handler and nothing else. Both are async in version 14.
For anything that arrives after a promise, use await screen.findByText(...), which retries until the element exists or the timeout expires, or waitFor for an assertion that is not a query. Matchers such as toBeOnTheScreen and toBeVisible ship with the library and need no setup file.
Example
// __tests__/SignInForm.test.tsx
import { render, screen, userEvent } from '@testing-library/react-native';
import SignInForm from '../components/SignInForm';
describe('<SignInForm />', () => {
it('reports a bad email instead of submitting', async () => {
const onSubmit = jest.fn();
const user = userEvent.setup();
await render(<SignInForm onSubmit={onSubmit} />);
// Queried the way a user identifies them, not by component structure.
await user.type(screen.getByPlaceholderText('[email protected]'), 'nope');
await user.press(screen.getByRole('button', { name: 'Sign in' }));
// findBy* retries until the element appears or the timeout expires.
expect(await screen.findByText('Enter a valid email address')).toBeOnTheScreen();
expect(onSubmit).not.toHaveBeenCalled();
});
it('hands the trimmed values to onSubmit', async () => {
const onSubmit = jest.fn();
const user = userEvent.setup();
await render(<SignInForm onSubmit={onSubmit} />);
await user.type(screen.getByPlaceholderText('[email protected]'), ' [email protected] ');
await user.type(screen.getByPlaceholderText('Password'), 'correct-horse');
await user.press(screen.getByRole('button', { name: 'Sign in' }));
expect(onSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'correct-horse',
});
});
});When to use it
- Check that a form shows a validation message and refuses to submit.
- Prove a list screen replaces its loading state with rows once data arrives.
- Verify a disabled button does not call its handler when pressed.
- Confirm a toggle announces its state to a screen reader as well as changing colour.
More examples
Writing a component that is queryable
On screen this is a padded card: the label, the number below it, and an "Add one" row that raises the number by one per tap. The two accessibility props are not test scaffolding; they are what a screen reader announces, and the test gets to use them for free. That is the whole argument for querying by role and label: the properties that make a component testable are the ones that make it usable.
// components/CounterCard.tsx
import { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
type CounterCardProps = { label: string };
export default function CounterCard({ label }: CounterCardProps) {
const [count, setCount] = useState(0);
return (
<View style={styles.card}>
<Text style={styles.label}>{label}</Text>
{/* accessibilityLabel gives the number a stable name to query by. */}
<Text accessibilityLabel="current count">{count}</Text>
{/* Without accessibilityRole, getByRole('button') cannot find this. */}
<Pressable accessibilityRole="button" onPress={() => setCount((c) => c + 1)}>
<Text>Add one</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
card: { padding: 16, gap: 8 },
label: { fontWeight: '600' },
});fireEvent and userEvent side by side
The two forms do the same job for a simple press, and you will meet both in other people's code. fireEvent is a direct call to one handler, so it ignores whether the element is disabled or covered. userEvent walks the real sequence, which is what you want for a TextInput (focus, keystrokes, blur) or anywhere onPressIn and onPressOut matter. Prefer userEvent by default and drop to fireEvent for an event it does not model, such as a raw onLayout.
// __tests__/CounterCard.events.test.tsx
import { fireEvent, render, screen, userEvent } from '@testing-library/react-native';
import CounterCard from '../components/CounterCard';
it('counts a press fired either way', async () => {
await render(<CounterCard label="Taps" />);
const button = screen.getByRole('button', { name: 'Add one' });
const value = () => screen.getByLabelText('current count');
// Low level: invokes the onPress prop and nothing else.
await fireEvent.press(button);
expect(value()).toHaveTextContent('1');
// Higher level: emits the whole touch sequence a finger produces.
const user = userEvent.setup();
await user.press(button);
expect(value()).toHaveTextContent('2');
});Waiting for an async update
Use findBy* when you are waiting for an element and waitFor when you are waiting for something else, such as a spy having been called. Never reach for a fixed sleep: it is slower than waitFor on a fast machine and still too short on a loaded CI runner. Note the last line uses queryBy*, which returns null, because getBy* throws when nothing matches and so can never prove absence.
// __tests__/ProfileScreen.test.tsx
import { render, screen, waitFor } from '@testing-library/react-native';
import ProfileScreen from '../components/ProfileScreen';
it('swaps the spinner for the name', async () => {
await render(<ProfileScreen userId="u_1" />);
// Present on the first render, before the request resolves.
expect(screen.getByLabelText('Loading profile')).toBeOnTheScreen();
// findBy* is getBy* wrapped in waitFor. These two lines are equivalent.
expect(await screen.findByText('Ada Lovelace')).toBeOnTheScreen();
await waitFor(() => expect(screen.getByText('Ada Lovelace')).toBeOnTheScreen());
expect(screen.queryByLabelText('Loading profile')).not.toBeOnTheScreen();
});
Discussion