Skip to main content
Testing is crucial for building reliable React Native applications. This guide covers unit testing, integration testing, and end-to-end testing strategies.

Testing Framework

React Native uses Jest as its default testing framework. Jest is pre-configured in React Native projects.

Jest Configuration

React Native’s Jest configuration is located in jest.config.js:
module.exports = {
  preset: 'react-native',
  setupFiles: ['./jest.setup.js'],
  testRegex: '/__tests__/.*-test\\.js$',
  transformIgnorePatterns: [
    'node_modules/(?!@react-native|react-native)',
  ],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
};

Unit Testing Components

React Native Testing Library

Install React Native Testing Library:
npm install --save-dev @testing-library/react-native @testing-library/jest-native

Setup

Create jest.setup.js:
import '@testing-library/jest-native/extend-expect';

// Mock native modules
jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper');

Basic Component Test

import React from 'react';
import {render, fireEvent, waitFor} from '@testing-library/react-native';
import {Button} from './Button';

describe('Button Component', () => {
  it('renders correctly', () => {
    const {getByText} = render(<Button title="Click Me" />);
    expect(getByText('Click Me')).toBeTruthy();
  });

  it('handles press events', () => {
    const onPress = jest.fn();
    const {getByText} = render(
      <Button title="Click Me" onPress={onPress} />
    );
    
    fireEvent.press(getByText('Click Me'));
    expect(onPress).toHaveBeenCalledTimes(1);
  });

  it('disables button when loading', () => {
    const {getByText} = render(
      <Button title="Submit" loading={true} />
    );
    
    const button = getByText('Submit');
    expect(button).toBeDisabled();
  });
});

Testing Async Operations

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

it('loads and displays user data', async () => {
  const {getByText, getByTestId} = render(<UserProfile userId="123" />);
  
  // Initially shows loading
  expect(getByTestId('loading-indicator')).toBeTruthy();
  
  // Wait for data to load
  await waitFor(() => {
    expect(getByText('John Doe')).toBeTruthy();
  });
  
  // Loading indicator should be gone
  expect(() => getByTestId('loading-indicator')).toThrow();
});

Mocking Native Modules

Manual Mocks

Create __mocks__ directory:
__mocks__/
  react-native.js
  @react-native-async-storage/
    async-storage.js

Mock React Native Modules

// __mocks__/react-native.js
const ReactNative = jest.requireActual('react-native');

ReactNative.NativeModules.StatusBarManager = {
  HEIGHT: 42,
  setStyle: jest.fn(),
};

ReactNative.Platform.OS = 'ios';

module.exports = ReactNative;

Mock AsyncStorage

// __mocks__/@react-native-async-storage/async-storage.js
const asyncStorage = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn(),
};

export default asyncStorage;
Usage in tests:
import AsyncStorage from '@react-native-async-storage/async-storage';

beforeEach(() => {
  AsyncStorage.getItem.mockClear();
});

it('saves data to storage', async () => {
  AsyncStorage.setItem.mockResolvedValue(null);
  
  await saveUserData({name: 'John'});
  
  expect(AsyncStorage.setItem).toHaveBeenCalledWith(
    'userData',
    '{"name":"John"}'
  );
});

Testing Navigation

React Navigation Mocking

import {render} from '@testing-library/react-native';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';

const Stack = createStackNavigator();

const renderWithNavigation = (component, {route = {}} = {}) => {
  return render(
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Test" component={() => component} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

it('navigates to details screen', () => {
  const navigation = {
    navigate: jest.fn(),
  };
  
  const {getByText} = render(
    <HomeScreen navigation={navigation} />
  );
  
  fireEvent.press(getByText('View Details'));
  expect(navigation.navigate).toHaveBeenCalledWith('Details', {id: 1});
});

Testing Hooks

renderHook Utility

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

it('increments counter', () => {
  const {result} = renderHook(() => useCounter());
  
  expect(result.current.count).toBe(0);
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);
});

Testing Custom Hooks with Dependencies

import {renderHook} from '@testing-library/react-hooks';
import {useFetchUser} from './useFetchUser';

it('fetches user data', async () => {
  global.fetch = jest.fn(() =>
    Promise.resolve({
      json: () => Promise.resolve({name: 'John'}),
    })
  );
  
  const {result, waitForNextUpdate} = renderHook(() => 
    useFetchUser('123')
  );
  
  expect(result.current.loading).toBe(true);
  
  await waitForNextUpdate();
  
  expect(result.current.data).toEqual({name: 'John'});
  expect(result.current.loading).toBe(false);
});

Snapshot Testing

import React from 'react';
import renderer from 'react-test-renderer';
import {Button} from './Button';

it('renders correctly', () => {
  const tree = renderer
    .create(<Button title="Press Me" />)
    .toJSON();
  expect(tree).toMatchSnapshot();
});
Use snapshots sparingly. They can make tests brittle and hard to maintain. Prefer explicit assertions when possible.

Integration Testing

Testing User Flows

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

it('completes login flow', async () => {
  const onLoginSuccess = jest.fn();
  const {getByPlaceholderText, getByText} = render(
    <LoginScreen onLoginSuccess={onLoginSuccess} />
  );
  
  // Enter credentials
  fireEvent.changeText(
    getByPlaceholderText('Email'),
    '[email protected]'
  );
  fireEvent.changeText(
    getByPlaceholderText('Password'),
    'password123'
  );
  
  // Submit form
  fireEvent.press(getByText('Login'));
  
  // Wait for async operation
  await waitFor(() => {
    expect(onLoginSuccess).toHaveBeenCalled();
  });
});

End-to-End Testing

Detox

Detox is a popular E2E testing framework for React Native.

Installation

npm install --save-dev detox

Configuration

Add to package.json:
{
  "detox": {
    "configurations": {
      "ios.sim.debug": {
        "device": {
          "type": "iPhone 14"
        },
        "app": "ios.debug"
      },
      "android.emu.debug": {
        "device": {
          "avdName": "Pixel_5_API_31"
        },
        "app": "android.debug"
      }
    }
  }
}

E2E Test Example

describe('Login Flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should login successfully', async () => {
    await element(by.id('email-input')).typeText('[email protected]');
    await element(by.id('password-input')).typeText('password');
    await element(by.id('login-button')).tap();
    
    await expect(element(by.text('Welcome!'))).toBeVisible();
  });
});

Code Coverage

Running with Coverage

npm test -- --coverage

Coverage Configuration

Add to jest.config.js:
module.exports = {
  collectCoverageFrom: [
    'src/**/*.{js,jsx,ts,tsx}',
    '!src/**/*.test.{js,jsx,ts,tsx}',
    '!src/**/index.{js,ts}',
  ],
  coverageThresholds: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

Best Practices

  1. Test behavior, not implementation: Focus on what the component does, not how it does it
  2. Use data-testid sparingly: Prefer accessible queries like getByText or getByRole
  3. Mock external dependencies: Keep tests isolated and fast
  4. Write integration tests: Test how components work together
  5. Keep tests maintainable: Avoid brittle selectors and over-mocking
  6. Test error states: Don’t just test the happy path
  7. Use TypeScript: Get type safety in your tests

CI/CD Integration

GitHub Actions Example

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: codecov/codecov-action@v3

Next Steps

Fast Refresh

Enable instant feedback during development

Troubleshooting

Solve common testing issues

Build docs developers (and LLMs) love