Skip to main content

Testing React Native Apps

1. Writing Unit Tests

Example: Testing a simple function

import { calculateTotal } from './calculator';

test('calculates total correctly', () => {
  const items = [
    { price: 10, quantity: 2 },
    { price: 5, quantity: 1 }
  ];
  expect(calculateTotal(items)).toBe(25);
});
Test Framework Description Notes
Jest JavaScript testing framework Default for React Native projects
Mocha Flexible test framework Requires additional setup for React Native
Note: Jest is included by default in React Native projects created with npx react-native init.

Example: Mocking modules

jest.mock('react-native', () => {
  const actual = jest.requireActual('react-native');
  return {
    ...actual,
    Alert: {
      alert: jest.fn()
    }
  };
});

2. Testing Components

Example: Testing a Simple Component

import React from 'react';
import { render } from '@testing-library/react-native';
import { Text } from 'react-native';

const Greeting = ({ name }) => <Text>Hello, {name}!</Text>;

test('renders greeting', () => {
  const { getByText } = render(<Greeting name="John" />);
  expect(getByText('Hello, John!')).toBeTruthy();
});
Method Description Usage
render() Renders a component into the document render(<Component />)
getByText() Find elements by their text content getByText('Hello, John!')
getAllByText() Find all elements by their text content getAllByText('Hello')
queryByText() Find elements by their text content, returns null if not found queryByText('Hello, John!')
findByText() Async version to find elements by their text content findByText('Hello, John!')
Note: Use findByText for asynchronous content updates.

Example: Testing Button Press

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

const Counter = ({ onIncrement }) => (
  <Button onPress={onIncrement}>Press Me</Button>
);

test('increments counter on press', () => {
  const mockOnIncrement = jest.fn();
  const { getByText } = render(<Counter onIncrement={mockOnIncrement} />);
  fireEvent.press(getByText('Press Me'));
  expect(mockOnIncrement).toHaveBeenCalledTimes(1);
});

Testing Component Workflow

  1. Create a test file for your component (e.g., Component.test.js)
  2. Import the component and testing utilities
  3. Render the component using render()
  4. Find elements using query methods
  5. Assert expectations with Jest matchers
  6. Simulate user interactions using fireEvent

3. Mocking Native Modules

When testing React Native apps, mocking native modules is essential to simulate platform-specific behavior without requiring actual device or simulator environments.

Module Mocked Behavior Example Usage
NativeModules Simulated native module responses jest.mock('NativeModules', () => ({
  MyNativeModule: {
    method: jest.fn(() => Promise.resolve('mocked'))
  }
}))

Example: Mocking a Native Module

import { NativeModules } from 'react-native';

// Mock the native module
jest.mock('NativeModules', () => ({
  MyNativeModule: {
    fetchUserData: jest.fn(() => Promise.resolve({ id: 1, name: 'John Doe' })),
    saveUserSettings: jest.fn(() => Promise.resolve(true))
  }
}));

// In test
test('should fetch user data', async () => {
  const userData = await NativeModules.MyNativeModule.fetchUserData();
  expect(userData.name).toBe('John Doe');
});
Note: Ensure that mocked modules are properly reset between tests to avoid side effects.
Warning: Mocking native modules can lead to discrepancies between test and production behavior if not carefully implemented.

4. Testing Hooks

Hook Description Usage Notes
useEffect Performs side effects in functional components
useEffect(() => {
  // Runs after render
  return () => {
    // Cleanup
  };
}, [dependencies]);
Commonly used for API calls, subscriptions, and cleanup
useState Manages local state in functional components const [state, setState] = useState(initialValue); Used for simple state updates and re-renders
useContext Accesses React context values const value = useContext(MyContext); Useful for global state or theme providers
useReducer Manages complex state logic using reducer functions const [state, dispatch] = useReducer(reducer, initialState); Better for state logic with multiple sub-states or actions
useCallback Returns a memoized version of a callback function
const memoizedCallback = useCallback(() => {
  // Some logic
}, [dependencies]);
Prevents unnecessary re-renders in child components
useMemo Returns a memoized value const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); Optimizes performance by caching expensive computations

Example: Testing useEffect

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

const ComponentWithEffect = () => {
  const [data, setData] = useState('');

  useEffect(() => {
    fetch('/api/data')
      .then(res => res.json())
      .then(setData);
  }, []);

  return <Text>{data}</Text>;
};

// Test
test('renders data after effect', async () => {
  const { findByText } = render(<ComponentWithEffect />);
  expect(await findByText('fetched data')).toBeTruthy();
});

Example: Testing useState

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

const CounterComponent = () => {
  const [count, setCount] = useState(0);

  return (
    <>
      <Text testID="count">{count}</Text>
      <Button title="Increment" onPress={() => setCount(c => c + 1)} />
    </>
  );
};

// Test
test('increments count on button press', () => {
  const { getByTestId, getByText } = render(<CounterComponent />);
  const countText = getByTestId('count');
  const button = getByText('Increment');

  expect(countText.props.children).toBe(0);
  button.props.onPress();
  expect(countText.props.children).toBe(1);
});
Note: When testing hooks that depend on external libraries or APIs, consider mocking those dependencies to isolate the hook's behavior.

5. Writing Integration Tests

Example: Setting up integration test environment

import { render } from &#x27;@testing-library/react-native&#x27;;
import App from &#x27;../App&#x27;;

test(&#x27;renders app correctly&#x27;, () => {
  const { getByText } = render(<App />);
  expect(getByText(&#x27;Welcome to React Native&#x27;)).toBeTruthy();
});
Test Type Description Tools
Integration Test Tests interaction between components and external systems @testing-library/react-native, jest
Note: Integration tests verify that components work together as expected in a realistic environment.

Best Practices for Integration Tests

  • Use render from @testing-library/react-native to mount components
  • Test with real or mocked external dependencies (APIs, databases)
  • Ensure tests are isolated and do not depend on each other
  • Use descriptive test names to reflect the behavior being tested

6. Testing Navigation

Example: Testing Navigation with React Navigation

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

const Stack = createStackNavigator();

function TestNavigation() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Detail" component={DetailScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

test('navigates to detail screen', () => {
  const { getByText } = render(<TestNavigation />);
  const homeScreen = getByText('Home Screen');
  expect(homeScreen).toBeTruthy();
});
Feature Description Testing Approach
Navigation Events Events triggered during navigation Mock navigation props and assert event calls
Screen Transitions Transitions between screens Use navigation.navigate in tests with jest
Deep Linking Handling deep links to specific screens Test with NavigationContainer and initialState
  • Use render from @testing-library/react-native to mount components
  • Wrap your app in NavigationContainer for navigation tests
  • Mock screen components to isolate navigation logic
  • Use jest to spy on navigation methods like navigate
  • Test screen transitions with navigation.navigate and assert state changes

7. Implementing Snapshot Testing

Example: Basic Snapshot Test

import renderer from 'react-test-renderer';
import MyComponent from './MyComponent';

test('renders correctly', () => {
  const tree = renderer.create(<MyComponent />).toJSON();
  expect(tree).toMatchSnapshot();
});
Method Description Usage
toMatchSnapshot() Matches the current output to a stored snapshot Call on rendered component
toMatchInlineSnapshot() Matches to an inline snapshot in the test file Use for small, simple snapshots
Note: Snapshots are stored in .snap files and should be committed to version control.
Warning: Snapshot tests can be brittle if component structure changes frequently. Update snapshots with jest --updateSnapshot.

Best Practices for Snapshot Testing

  • Use snapshots for UI regression testing
  • Keep snapshots simple and focused
  • Update snapshots regularly to reflect UI changes
  • Avoid snapshotting dynamic or random content

8. Testing Async Code

Method Description Usage
waitFor Waits for a condition to be true before continuing await waitFor(() => expect(mockFetch).toBeCalled());
act Ensures all updates are processed before assertions
act(() => {
  renderer.update(<Component />);
});
expect(renderer.root.findByType('Text')).toBeTruthy();

Example: Testing Async Fetch

it('fetches user data', async () => {
  const mockUser = { id: 1, name: 'John' };
  jest.spyOn(global, 'fetch').mockResolvedValueOnce({
    json: jest.fn().mockResolvedValue(mockUser),
  });

  const { getByText } = render(<UserProfile userId={1} />);
  await waitFor(() => getByText('John'));
});
Note: Use waitFor to handle asynchronous operations in tests.

9. Using Test Renderers

Test renderers in React Native allow you to test components without a physical device or emulator. They provide a virtual DOM environment for unit testing.

Renderer Description Use Case Support
React Test Renderer Official test renderer provided by React team Component rendering and snapshot testing NEW in React 18
Shallow Renderer Only renders the component at the top level, not its children Isolated unit testing of components DEPRECATED in React 18

Example: Using React Test Renderer

import React from 'react';
import TestRenderer from 'react-test-renderer';

const MyComponent = ({ name }) => (
  <View>
    <Text>Hello, {name}!</Text>
  </View>
);

const renderer = TestRenderer.create(<MyComponent name="John" />);
console.log(renderer.toJSON());
Note: The react-test-renderer is ideal for snapshot testing and component rendering without a full DOM.
Warning: Shallow rendering is deprecated and should be replaced with React Test Renderer for better compatibility.

10. Implementing E2E Testing

Example: Setting up Detox for E2E Testing

// Install Detox
npm install --save-dev detox

// Configure detox in package.json
{
  "detox": {
    "configurations": {
      "ios.sim": {
        "binaryPath": "ios/build/Build/Products/Release-iphonesimulator/MyApp.app",
        "build": "xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Release -sdk iphonesimulator -derivedDataPath ios/build",
        "type": "ios.simulator"
      },
      "android.emu": {
        "binaryPath": "android/app/build/outputs/apk/release/app-release.apk",
        "build": "cd android && ./gradlew assembleRelease assembleAndroidTest -PisLocalBuild=true",
        "type": "android.emulator"
      }
    },
    "testCommand": "detox test --configuration ios.sim",
    "runner": "detox"
  }
}
Configuration Description Values
binaryPath Path to the built app binary ios/build/Build/Products/Release-iphonesimulator/MyApp.app
build Build command to compile the app xcodebuild ...
type Platform type (ios.simulator, android.emulator) ios.simulator
Note: Ensure that the app is built in Release mode for E2E tests to avoid debug overhead.

Example: Writing a Detox Test

describe('Example Test Suite', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should show the welcome screen', async () => {
    await expect(element(by.id('welcomeScreen'))).toBeVisible();
  });

  it('should navigate to the settings screen', async () => {
    await element(by.id('settingsButton')).tap();
    await expect(element(by.id('settingsScreen'))).toBeVisible();
  });
});

Key Steps in E2E Testing Setup

  • Install Detox or other E2E testing tools
  • Configure build scripts for iOS and Android
  • Write test cases using Detox API or similar
  • Run tests on emulators/simulators

11. Testing on Real Devices

Test Type Description Tools Notes
Device Testing Running tests on physical devices to ensure real-world behavior react-native test, jest Requires device setup and configuration
E2E Testing End-to-end testing using tools like Detox detox, appium Simulates user interactions
Performance Testing Measures app performance on real devices react-native-performance, Flipper Monitors CPU, memory usage

Example: Setting up Detox for E2E Tests

const detox = require('detox');
const config = require('../package.json').detox;

beforeAll(async () => {
  await detox.init(config);
});

afterAll(async () => {
  await detox.cleanup();
});
Note: Detox requires a device or emulator to run tests. Ensure the device is properly configured with necessary permissions.
Warning: Running tests on real devices can be slower than emulators due to hardware limitations.

Key Points for Device Testing

  • Use physical devices to simulate real user conditions
  • Detox is a popular choice for E2E testing on real devices
  • Performance metrics should be collected during device tests

12. Using Maestro for E2E Testing

Example: Setting up Maestro for React Native

npm install -g @maestro/cli
Feature Description Status
Test Recording Record user interactions to generate test scenarios NEW
Cross-Platform Support Run tests on both iOS and Android EXPERIMENTAL
Test Assertions Verify UI elements and app states STABLE
Note: Maestro requires a running React Native app instance to connect and execute tests.
Warning: Ensure all device permissions are granted before running E2E tests with Maestro.

Workflow: Running Maestro Tests

  1. Start your React Native app in development mode
  2. Run maestro test command with your test scenario file
  3. View results in the terminal or Maestro dashboard

Comparison: Maestro vs Detox

  • Maestro: Simpler setup, uses YAML for test scenarios
  • Detox: More complex but offers deeper integration with React Native

Was this article helpful?