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.

Note: every test file above is a correct, static example meant to be added to your own project's test suite — this site doesn't run a React Native build or a live Jest process on this page.
Next up: once your app is tested and working, it's time to produce an actual build users can install. Continue to Building & Releasing.