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
- Create a test file for your component (e.g.,
Component.test.js) - Import the component and testing utilities
- Render the component using
render() - Find elements using query methods
- Assert expectations with Jest matchers
- 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 | |
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 | |
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 '@testing-library/react-native';
import App from '../App';
test('renders app correctly', () => {
const { getByText } = render(<App />);
expect(getByText('Welcome to React Native')).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
renderfrom@testing-library/react-nativeto 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 |
Navigation Testing Best Practices
- Use
renderfrom@testing-library/react-nativeto mount components - Wrap your app in
NavigationContainerfor navigation tests - Mock screen components to isolate navigation logic
- Use
jestto spy on navigation methods likenavigate - Test screen transitions with
navigation.navigateand 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 | |
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
| 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.