Why Unit Testing Matters

Unit testing verifies that the smallest testable parts of a software application work as expected. In React Native, a “unit” is typically a component. A basic example is a Button component that accepts text and an onPress function:

import React from 'react';
import { StyleSheet, Text, TouchableOpacity } from 'react-native';
function AppButton({ onPress }) {
    return (
      <TouchableOpacity
          style={[styles.button,
              { backgroundColor: colors[color] }]}
                 onPress={onPress} >
          <Text style={styles.text}>Register</Text>
      </TouchableOpacity>
    );
}
const styles = StyleSheet.create({
    button: {
        backgroundColor: red;
        borderRadius: 25,
        justifyContent: 'center',
        alignItems: 'center',
    },
    text: {
        color: #fff
    }
})
export default AppButton;

Unit testing this component means verifying it renders correctly regardless of where it appears in the app. A corresponding test file (Button.test.js) can check that rendering:

it('renders correctly across screens', () => {
  const tree = renderer.create(<Button />).toJSON();
  expect(tree).toMatchSnapshot();
});

Choosing a Testing Stack for React Native

Several tools work well with React Native. Each has its strengths:

  • Jest: A popular “zero config” framework from Facebook. It’s the focus of this guide due to its performance and low setup overhead.
  • Enzyme: A JavaScript testing utility for React and React Native that makes it easy to assert, manipulate, and traverse your components’ output.
  • Mocha: A flexible test framework known for fast, straightforward setup.
  • Jasmine: A behavior-driven development framework for testing JavaScript code.

To start, we’ll use the Done With It React Native marketplace app from GitHub. Clone the repository, install dependencies with your package manager, and you are ready to test.

npm install

or, with Yarn:

yarn install

Configuring Jest and Writing Snapshot Tests

To enable Jest in an Expo-based React Native project, install the jest-expo preset.

yarn add jest-expo --dev

Then, add a test script to package.json telling Jest which preset to use.

"scripts": {
    "test" "jest"
},
"jest": {
    "preset": "jest-expo"
}

Install additional testing dependencies to support comprehensive tests.

npm i react-test-renderer --save-dev

or with Yarn:

yarn add react-test-renderer --dev

Add a transformIgnorePattern configuration to your package.json. This avoids running tests on matching source files in the project’s node_modules.

"jest": {
  "preset": "jest-expo",
  "transformIgnorePatterns": [
    "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
  ]
}

Now create an App.test.js file. The first test checks whether the App component has exactly one child element in its tree. This uses react-test-renderer to convert the component tree into JSON, then Jest compares the result with your expectation.

import React from "react";
import renderer from "react-test-renderer";
import App from "./App.js"
describe("<App />", () => {
    it('has 1 child', () => {
        const tree = renderer.create(<App />).toJSON();
        expect(tree.children.length).toBe(1);
    });
});

Snapshot tests capture a component’s rendered output (its code) at a moment in time. This is particularly useful when a project relies on global styles shared across components. Add a snapshot test for App.js to your test suite.

it('renders correctly across screens', () => {
  const tree = renderer.create().toJSON();
  expect(tree).toMatchSnapshot();
});

Run the tests with yarn test (or the npm equivalent). A passing suite will confirm the component’s UI remains consistent.

  PASS  src/App.test.js
  √ has 1 child (16ms)
  √ renders correctly (16ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   1 total
Time:        24s

Mocking API Calls

Mock functions let you test the links between pieces of code by replacing a function’s implementation. They capture calls and their parameters, enabling you to control return values at runtime. The main benefit is that you reduce your tests' dependency on external systems like live APIs.

There are two common approaches: creating a mock function to inject into the code, or writing a mock that overrides a package dependency. To avoid real API calls in React Native unit tests, mock the global fetch:

global.fetch = jest.fn();

// mocking an API success response once
fetch.mockResponseIsSuccess = (body) => {
  fetch.mockImplementationForOnce (
    () => Promise.resolve({json: () => Promise.resolve(JSON.parse(body))})
  );
};

// mocking an API failure response for once
fetch.mockResponseIsFailure = (error) => {
  fetch.mockImplementationForOnce(
    () => Promise.reject(error)
  );
};

This example attempts a single fetch, returns a promise, and on resolution parses the body as JSON. A failed request similarly returns an error.

Consider a product component that receives product data as props. Instead of accessing a database, mock that data for tests. Define the mocked props and use Jest’s describe to structure the tests.

import React from 'react';
const Product = () => {
    const product = {
        name: 'Pizza',
        quantity: 5,
        price: '$50'
    }
    return (
        <>
            <h1>Name: {product.name}</h1>   
            <h1>Quantity: {product.quantity}</h1>   
            <h1>Price: {product.price}</h1>   
        </>
    );
}
export default Product;
describe("", () => {
  it("accepts products props", () => {
    const wrapper = mount(<Customer product={product} />);
    expect(wrapper.props().product).toEqual(product);
  });
  it("contains products quantity", () => {
    expect(value).toBe(3);
  });
});

These tests confirm that passed objects match the mocked props and that the component behaves correctly given the expected data. This prevents bugs without testing every possible product variant.

Mocking External API Requests

When testing API calls, use axios-mock-adapter to simulate requests and manage responses. First, install the adapter.

yarn add axios-mock-adapter

The mock interacts with an Axios instance and simulates the authentication endpoint. It uses faker.js to generate fake user data for credentials.

import MockAdapter from 'axios-mock-adapter';
import Faker from 'faker'
import ApiClient from '../constants/api-client';
import userDetails from 'jest/mockResponseObjects/user-objects';

let mockApi = new MockAdapter(ApiClient.getAxiosInstance());
let validAuthentication = {
    name: Faker.internet.email(),
    password: Faker.internet.password()

mockApi.onPost('requests').reply(config) => {
  if (config.data ===  validAuthentication) {
      return [200, userDetails];
    }
  return [400, 'Incorrect username and password'];
 });

The mock acts exactly like the real API. A successful request yields a 200 OK status. Invalid credentials (wrong email or password) produce a 400 status with an error message.

Test both paths using async/await, keeping the authenticateUser function in check with a snapshot.

it('successful sign in with correct credentials', async () => {
  await store.dispatch(authenticateUser('[email protected]', 'password'));
  expect(getActions()).toMatchSnapshot();
});

it('unsuccessful sign in with wrong credentials', async () => {
  await store.dispatch(authenticateUser('[email protected]', 'wrong credential'))
  .catch((error) => {
    expect(errorObject).toMatchSnapshot();
  });

Testing Redux State Management

Testing Redux can get complex quickly. Most of the test logic involving Redux is about two core elements:

  • Create a redux-mock-store and dispatch actions to test them.
  • Import the reducer, passing a current state and an action object to verify the resulting state.

Snapshots simplify the process. This test covers the SIGN-IN and LOGOUT actions. It first builds a mock store, then a mocked user from testUser. The snapshot verifies that the dispatch object matches each time during a successful sign-in. The logout test checks that the reducer returns the app to its initial state.

import mockStore from 'redux-mock-store';
import { LOGOUT } from '../actions/logout';
import User from '../reducers/user';
import { testUser } from 'jest/mock-objects';

  describe('Testing the sign in authentication', () => {
    const store = mockStore();

  it('user attempts with correct password and succeeds', async () => {
  await store.dispatch(authenticateUser('[email protected]', 'password'));
  expect(store.getActions()).toMatchSnapshot();
  });
});
  describe('Testing reducers after user LOGS OUT', () => {
    it('user is returned back to initial app state', () => {
      expect(user(testUser, { type: LOGOUT })).toMatchSnapshot();
    });
  });

After running the suite, a successful result appears as expected.

  PASS  src/redux/actions.test.js
  √ user attempts with correct password and succeeds (23ms)
  √ user is returned back to initial app state (19ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   2 total
Time:        31s

Keeping a React Native UI Stable

Snapshot tests give you a straightforward way to protect the UI from unintended changes. Because Jest’s snapshot mechanism operates independently of global style tweaks, it’s ideal for catching accidental regressions in a React Native project. If a component's rendered output changes unexpectedly, the snapshot test fails immediately, pointing you to the exact difference.

Isolating Components With Mocks

Unit testing in React Native often requires cutting off external dependencies. Jest makes this manageable through built-in mocking support. You can simulate API responses and stub entire modules, so each test focuses solely on the component’s behavior and logic. This isolation is key when you want to verify that a component renders correctly under various conditions or fires the right callbacks.

Moving Beyond Render Checks

Once the basics are in place, you can extend your test suite to exercise deeper component logic. This means verifying state updates, validating user interactions, and checking conditional rendering paths. The combination of snapshot tests for UI consistency, mocks for isolation, and behavior-focused tests gives you solid coverage for a React Native application.

For further reading, refer to the following: