React Native Testing

Testing catches bugs before users find them. A tested app is a reliable app. React Native supports three types of testing: unit tests for individual functions, component tests for UI rendering, and end-to-end tests for full user flows. This topic covers the tools and patterns that professional teams use.

Three Testing Levels

Level                What It Tests              Tool
──────────────────────────────────────────────────────────────────
Unit Tests           Pure functions, hooks      Jest
Component Tests      UI rendering, user events  React Native Testing Library
End-to-End (E2E)     Full app user flows        Detox / Maestro

Testing Pyramid:
        ▲  E2E Tests  (few, slow, high confidence)
       ▲▲▲ Component Tests (moderate count)
     ▲▲▲▲▲▲▲ Unit Tests (many, fast, isolated)

Jest — Unit Testing

Expo projects include Jest pre-configured. Jest runs your test files and reports which pass or fail. A unit test checks one piece of logic in isolation — no network, no device, no navigation.

Writing Your First Unit Test

// utils/formatPrice.js
export function formatPrice(amount, currency = 'USD') {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
  }).format(amount);
}

// utils/__tests__/formatPrice.test.js
import { formatPrice } from '../formatPrice';

describe('formatPrice', () => {
  test('formats a whole number in USD', () => {
    expect(formatPrice(10)).toBe('$10.00');
  });

  test('formats a decimal amount', () => {
    expect(formatPrice(9.99)).toBe('$9.99');
  });

  test('formats in EUR', () => {
    expect(formatPrice(25, 'EUR')).toBe('€25.00');
  });

  test('formats zero', () => {
    expect(formatPrice(0)).toBe('$0.00');
  });
});
Run tests:
$ npx jest

Output:
  ✓ formats a whole number in USD    (2ms)
  ✓ formats a decimal amount         (1ms)
  ✓ formats in EUR                   (1ms)
  ✓ formats zero                     (0ms)

Test Suites: 1 passed
Tests:       4 passed

Jest Matchers

expect(value).toBe(exact)         // strict equality (===)
expect(value).toEqual(obj)        // deep equality (objects/arrays)
expect(value).toBeTruthy()        // any truthy value
expect(value).toBeFalsy()         // null, undefined, 0, false, ''
expect(value).toBeNull()          // exactly null
expect(value).toContain(item)     // array or string contains item
expect(fn).toThrow()              // function throws an error
expect(fn).toHaveBeenCalled()     // mock function was called
expect(fn).toHaveBeenCalledWith(a, b) // called with specific args

React Native Testing Library — Component Tests

React Native Testing Library (RNTL) renders components without a real device and lets you query and interact with them.

npx expo install @testing-library/react-native

Testing a Counter Component

// components/__tests__/Counter.test.js
import { render, screen, fireEvent } from '@testing-library/react-native';
import Counter from '../Counter';

describe('Counter', () => {
  test('displays initial count of 0', () => {
    render(<Counter />);
    expect(screen.getByText('0')).toBeTruthy();
  });

  test('increments count when button is pressed', () => {
    render(<Counter />);

    const button = screen.getByText('Tap to Increase');
    fireEvent.press(button);

    expect(screen.getByText('1')).toBeTruthy();
  });

  test('increments three times correctly', () => {
    render(<Counter />);
    const button = screen.getByText('Tap to Increase');

    fireEvent.press(button);
    fireEvent.press(button);
    fireEvent.press(button);

    expect(screen.getByText('3')).toBeTruthy();
  });
});

Testing a Form

// components/__tests__/LoginForm.test.js
import { render, screen, fireEvent } from '@testing-library/react-native';
import LoginForm from '../LoginForm';

test('shows error when form submitted empty', () => {
  render(<LoginForm />);
  const loginButton = screen.getByText('Login');
  fireEvent.press(loginButton);
  expect(screen.getByText('Please fill in all fields.')).toBeTruthy();
});

test('accepts typed email input', () => {
  render(<LoginForm />);
  const emailInput = screen.getByPlaceholderText('Email');
  fireEvent.changeText(emailInput, 'test@example.com');
  expect(emailInput.props.value).toBe('test@example.com');
});

RNTL Query Methods

screen.getByText('Login')              // find by visible text
screen.getByPlaceholderText('Email')   // find by placeholder
screen.getByTestId('submit-btn')       // find by testID prop
screen.getByRole('button')             // find by accessibility role
screen.queryByText('Error')            // returns null if not found
screen.findByText('Loaded')            // async — waits for element

fireEvent Methods

fireEvent.press(element)               // simulate a tap
fireEvent.changeText(input, 'value')   // simulate typing
fireEvent.scroll(list, { ... })        // simulate scroll

Testing Async Components

import { render, screen, waitFor } from '@testing-library/react-native';

// Mock the fetch call
global.fetch = jest.fn(() =>
  Promise.resolve({
    ok: true,
    json: () => Promise.resolve([{ id: 1, title: 'First Post' }]),
  })
);

test('loads and displays posts', async () => {
  render(<PostsScreen />);

  // Loading spinner should be visible first
  expect(screen.getByTestId('loading-spinner')).toBeTruthy();

  // Wait for data to appear
  await waitFor(() => {
    expect(screen.getByText('First Post')).toBeTruthy();
  });
});

Testing Custom Hooks

import { renderHook, act } from '@testing-library/react-native';
import { useCounter } from '../hooks/useCounter';

test('increments the count', () => {
  const { result } = renderHook(() => useCounter());

  expect(result.current.count).toBe(0);

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Mocking Modules

Tests run without a real device. Mock external modules like navigation, AsyncStorage, or expo-location to keep tests fast and deterministic.

// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage', () => ({
  getItem: jest.fn(() => Promise.resolve(null)),
  setItem: jest.fn(() => Promise.resolve()),
  removeItem: jest.fn(() => Promise.resolve()),
}));

// Mock navigation
const mockNavigate = jest.fn();
jest.mock('@react-navigation/native', () => ({
  useNavigation: () => ({ navigate: mockNavigate }),
}));

Test Coverage

npx jest --coverage

Output:
File                 | % Stmts | % Branch | % Funcs | % Lines
---------------------|---------|----------|---------|--------
formatPrice.js       |    100  |    100   |    100  |    100
Counter.js           |     95  |     80   |    100  |     95

Aim for:
Critical business logic  → 90%+ coverage
UI components            → 70%+ coverage
Utility functions        → 100% coverage

Summary

Write unit tests for all pure functions and custom hooks using Jest. Use React Native Testing Library to test component rendering and user interactions. Mock network calls, storage, and navigation to keep tests fast and isolated. Run --coverage to find untested code paths. Focus testing effort on critical business logic first, then expand to UI components.

Leave a Comment