Testing
Manually reopening the Developer Menu and clicking through your app after every change doesn't scale once an app grows past a handful of screens. Automated tests catch regressions — code changes that accidentally break something that used to work — before your users ever see them. React Native projects come ready-configured with everything you need to start testing on day one.
Jest: The Default Test Runner
React Native projects ship with Jest pre-configured as the test runner. Jest finds
files matching a test naming convention (typically *.test.js or files inside a
__tests__ folder), runs them, and reports which pass and which fail. A basic unit test
just checks that a plain function returns what you expect.
// sum.js
export function add(a, b) {
return a + b;
}
// sum.test.js
import { add } from './sum';
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
test() names the individual test case, and expect(...).toBe(...) is a
Jest "matcher" — one of many ways to assert that a value is what you expected. Plain functions like
add are the easiest thing to test, but most of what actually breaks in a real app lives
inside components, which need a different tool.
React Native Testing Library: Testing Components Like a User Would
React Native Testing Library is the standard way to test components — not by reaching into their internal state, but by rendering them and interacting with them the way an actual user would: finding a button by its visible text, pressing it, and checking what appears on screen afterward.
// Counter.js
import React, { useState } from 'react';
import { View, Text, Button } from 'react-native';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button title="Increment" onPress={() => setCount(count + 1)} />
</View>
);
}
// Counter.test.js
import React from 'react';
import { render, fireEvent, screen } from '@testing-library/react-native';
import Counter from './Counter';
test('increments the count when the button is pressed', () => {
render(<Counter />);
// Find the button by the text a user would actually see
const button = screen.getByText('Increment');
fireEvent.press(button);
// Assert on what the user would SEE, not on internal state
expect(screen.getByText('Count: 1')).toBeTruthy();
});
render() mounts the component in a test environment, screen.getByText()
finds elements the same way a user visually locates them, and fireEvent.press()
simulates an actual tap. This is the philosophy worth internalizing: test what the user sees and
does, not implementation details. Whether Counter stores its number in a state
variable called count, value, or n shouldn't matter to this
test at all — only that pressing "Increment" makes the displayed count go up. Tests written this
way survive internal refactors; tests that dig into component internals break every time you
rename something, even when the actual behavior hasn't changed.
Running Your Tests
Every React Native project generated from the standard templates comes with a test
script wired up already, so running the whole suite is a single command:
npm test
// or, with Yarn:
yarn test
Jest watches for changed files by default in most setups, re-running only the tests affected by your latest edit — handy for a tight feedback loop while you're actively working on a component.
Mocking a Module
Tests should be fast and deterministic — they shouldn't actually hit a real network, read real
device storage, or depend on anything outside your control. When a component uses
AsyncStorage (from Local Storage:
AsyncStorage) or makes a real network call (from
Networking: Fetch & Axios), you replace the real module with a fake ("mock") version for the
duration of the test.
// At the top of a test file: replace the real AsyncStorage with a fake one
jest.mock('@react-native-async-storage/async-storage', () => ({
getItem: jest.fn(() => Promise.resolve(null)),
setItem: jest.fn(() => Promise.resolve()),
}));
With the mock in place, any code under test that calls AsyncStorage.getItem() or
.setItem() talks to this fake in-memory version instead of touching real device
storage — keeping the test fast, isolated, and repeatable across machines and CI runs.
Snapshot Testing (Briefly, and With a Caveat)
Jest can also take a "snapshot" of a component's rendered output the first time a test runs, save it to a file, and on every future run compare the new output against that saved reference — failing the test if anything changed. It's a fast way to catch unintended changes to a component's structure. The honest caveat: snapshot tests become brittle and low-value if overused, since any intentional change to the component (even a harmless one) makes the snapshot "fail," and it's tempting to just blindly update the snapshot without actually checking whether the change was correct. Use snapshots sparingly, for components whose exact output you genuinely want to freeze — lean on Testing Library's behavior-focused tests (like the counter example above) for everything else.