← Back to Blog

React Native Testing Guide: Jest, Detox, and Testing Strategy

Mobile apps that aren't tested break in production. Here's how to build a practical testing pyramid for React Native — from unit tests to E2E.

Mobile developer testing React Native application

Testing React Native applications is more challenging than testing web apps — you're dealing with native rendering, platform-specific behavior, device permissions, and hardware integration. But untested mobile code is a liability: bugs reach production, regression happens during platform updates, and confidence in deployments erodes. This guide covers a practical testing strategy for React Native applications — what to test, what tools to use, and how to structure tests that actually provide value.

The React Native Testing Pyramid

The testing pyramid applies to mobile just as it does to web, but the layer definitions are different:

  • Unit tests (70%): Pure functions, custom hooks, business logic, utility functions. Fast, cheap, and deterministic. Run in Node.js with Jest.
  • Integration tests (20%): Components rendered with React Native Testing Library (RNTL), verifying interactions, state changes, and rendered output. Still run in Node.js.
  • End-to-end tests (10%): Full app flows on a real device or simulator using Detox. Slow and expensive — reserve for critical paths only.

The common mistake in mobile testing is inverting the pyramid — relying heavily on E2E tests that are slow, flaky, and expensive to maintain. Most of your confidence should come from fast unit and integration tests.

Unit Testing with Jest

Jest ships with React Native CLI projects and is the right tool for testing business logic. Configure it properly first:

// jest.config.js
module.exports = {
  preset: 'react-native',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
  transformIgnorePatterns: [
    'node_modules/(?!(react-native|@react-native|react-native-.*)/)',
  ],
  setupFilesAfterFramework: ['@testing-library/jest-native/extend-expect'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

Testing Custom Hooks

Custom hooks containing business logic are among the highest-value test targets — they encapsulate behavior that multiple components depend on:

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

describe('useCart', () => {
  it('adds items to cart', () => {
    const { result } = renderHook(() => useCart());

    act(() => {
      result.current.addItem({ id: '1', name: 'Widget', price: 9.99 });
    });

    expect(result.current.items).toHaveLength(1);
    expect(result.current.total).toBe(9.99);
  });

  it('removes items from cart', () => {
    const { result } = renderHook(() => useCart());

    act(() => {
      result.current.addItem({ id: '1', name: 'Widget', price: 9.99 });
      result.current.removeItem('1');
    });

    expect(result.current.items).toHaveLength(0);
    expect(result.current.total).toBe(0);
  });
});

Testing Pure Functions and Services

Any function that transforms data, formats output, or makes decisions without side effects should have 100% test coverage. These are the cheapest tests to write and the most reliable:

import { formatCurrency, calculateDiscount } from '../utils/pricing';

describe('formatCurrency', () => {
  it('formats positive amounts', () => {
    expect(formatCurrency(9.99)).toBe('$9.99');
  });
  it('formats zero', () => {
    expect(formatCurrency(0)).toBe('$0.00');
  });
  it('rounds to 2 decimal places', () => {
    expect(formatCurrency(9.999)).toBe('$10.00');
  });
});

Component Testing with React Native Testing Library

RNTL tests components by rendering them and simulating user interactions — without a real device. Install it:

npm install --save-dev @testing-library/react-native @testing-library/jest-native

Testing Component Rendering and Interactions

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

describe('LoginForm', () => {
  it('renders email and password inputs', () => {
    render(<LoginForm onSubmit={jest.fn()} />);

    expect(screen.getByPlaceholderText('Email')).toBeTruthy();
    expect(screen.getByPlaceholderText('Password')).toBeTruthy();
  });

  it('calls onSubmit with credentials', () => {
    const onSubmit = jest.fn();
    render(<LoginForm onSubmit={onSubmit} />);

    fireEvent.changeText(screen.getByPlaceholderText('Email'), 'user@test.com');
    fireEvent.changeText(screen.getByPlaceholderText('Password'), 'password123');
    fireEvent.press(screen.getByText('Log In'));

    expect(onSubmit).toHaveBeenCalledWith({
      email: 'user@test.com',
      password: 'password123',
    });
  });

  it('shows validation error for empty email', () => {
    render(<LoginForm onSubmit={jest.fn()} />);
    fireEvent.press(screen.getByText('Log In'));
    expect(screen.getByText('Email is required')).toBeTruthy();
  });
});

Mocking Native Modules

React Native modules that don't run in Node.js (camera, location, Bluetooth) need mocks:

// __mocks__/@react-native-camera/camera.js
export default {
  requestCameraPermission: jest.fn().mockResolvedValue('granted'),
  takePicture: jest.fn().mockResolvedValue({ uri: 'mock://photo.jpg' }),
};

// For NetInfo — mock online/offline states
jest.mock('@react-native-community/netinfo', () => ({
  addEventListener: jest.fn(),
  fetch: jest.fn().mockResolvedValue({ isConnected: true }),
}));

API Mocking with Mock Service Worker (MSW)

MSW intercepts API calls at the network level — more realistic than mocking fetch directly:

import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';

const server = setupServer(
  http.get('https://api.example.com/products', () => {
    return HttpResponse.json([
      { id: '1', name: 'Widget', price: 9.99 },
    ]);
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

it('displays products from API', async () => {
  render(<ProductList />);
  expect(await screen.findByText('Widget')).toBeTruthy();
});

End-to-End Testing with Detox

Detox runs your app on a real simulator/emulator and interacts with it programmatically. Reserve E2E tests for critical user journeys: login, checkout, core feature flows.

// e2e/login.test.js
describe('Login', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should login with valid credentials', async () => {
    await element(by.id('email-input')).typeText('user@test.com');
    await element(by.id('password-input')).typeText('password123');
    await element(by.id('login-button')).tap();
    await expect(element(by.id('home-screen'))).toBeVisible();
  });

  it('should show error for invalid credentials', async () => {
    await element(by.id('email-input')).typeText('wrong@test.com');
    await element(by.id('password-input')).typeText('wrongpass');
    await element(by.id('login-button')).tap();
    await expect(element(by.text('Invalid credentials'))).toBeVisible();
  });
});

CI/CD Integration

Unit and integration tests should run on every PR. E2E tests on every merge to main:

# .github/workflows/test.yml
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test -- --coverage --ci

  e2e-tests:
    runs-on: macos-latest  # Required for iOS simulator
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx detox build --configuration ios.sim.release
      - run: npx detox test --configuration ios.sim.release

Frequently Asked Questions

Should I test both iOS and Android in E2E?

For critical paths, yes. Platform-specific bugs are real — layout differences, gesture handling, native module behavior. Budget for E2E runs on both platforms. In CI, run iOS on every merge and Android on a nightly schedule if CI cost is a concern.

How do I test navigation?

Mock the navigation prop in component tests. For integration-level navigation testing, wrap components in a test navigator. For E2E, Detox handles navigation naturally since it's running the real app.

What coverage percentage should I target?

Coverage as a metric can mislead — 80% coverage that misses critical paths is worse than 60% coverage that tests everything important. Target 100% on business logic and utility functions, 70%+ on components, and focus E2E on the 3–5 user flows that generate the most value or carry the most risk.

Related Reading

Need help with your React Native testing strategy?

We audit mobile codebases, implement testing infrastructure, and build the CI/CD pipelines that give teams confidence to ship React Native apps on a fast cadence.

Let's Talk Mobile Quality