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
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
Test behavior, not implementation : Focus on what the component does, not how it does it
Use data-testid sparingly : Prefer accessible queries like getByText or getByRole
Mock external dependencies : Keep tests isolated and fast
Write integration tests : Test how components work together
Keep tests maintainable : Avoid brittle selectors and over-mocking
Test error states : Don’t just test the happy path
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