Mocking Native Modules in React Native Tests
Native modules are the bridge between your JavaScript code and platform APIs — camera, geolocation, biometrics, push notifications, Bluetooth. They're essential for real apps, but they're also the reason React Native unit tests break: native modules don't exist in a Jest environment running on Node.js.
The solution is mocking. By replacing native modules with controlled JavaScript equivalents, you can test all the code that depends on them without a device or emulator. This guide covers every pattern you'll need.
Why Native Modules Break Tests
When Jest runs your tests, it's executing JavaScript in Node.js. The React Native bridge — which enables JavaScript to call platform APIs — doesn't exist. Any code that imports or calls a native module will either throw an error or return undefined.
Error: Invariant Violation: TurboModuleRegistry.getEnforcing(...)or
TypeError: NativeModules.SomeModule is undefinedThe fix is always the same: intercept the import before the test runs and replace it with a mock.
Where to Put Mocks
Jest supports three locations for mocks:
1. Inline with jest.mock() — per-test file, most explicit:
jest.mock('react-native-camera', () => ({...}));2. __mocks__ directory — automatic for node_modules packages. Create __mocks__/react-native-camera.js and Jest picks it up automatically.
3. jest.setup.js — for mocks that apply across all tests. Reference in jest.config.js via setupFilesAfterFramework.
For React Native's built-in modules (from react-native), use the jest.setup.js or configure mock modules individually.
Mocking React Native's Built-in NativeModules
Some libraries access NativeModules directly:
import { NativeModules } from 'react-native';
const { MySdk } = NativeModules;
MySdk.initialize();Mock it:
jest.mock('react-native', () => {
const rn = jest.requireActual('react-native');
return {
...rn,
NativeModules: {
...rn.NativeModules,
MySdk: {
initialize: jest.fn(),
getUserId: jest.fn().mockResolvedValue('user-123'),
logout: jest.fn().mockResolvedValue(undefined),
},
},
};
});The jest.requireActual call is important — it preserves all the real React Native functionality you're not mocking.
Mocking AsyncStorage
@react-native-async-storage/async-storage is one of the most commonly mocked modules:
npm install --save-dev @react-native-async-storage/async-storage/jest/async-storage-mockIn jest.setup.js:
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';
jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);Or mock it manually with more control:
const mockStorage = {};
jest.mock('@react-native-async-storage/async-storage', () => ({
setItem: jest.fn((key, value) => {
mockStorage[key] = value;
return Promise.resolve();
}),
getItem: jest.fn((key) => Promise.resolve(mockStorage[key] || null)),
removeItem: jest.fn((key) => {
delete mockStorage[key];
return Promise.resolve();
}),
clear: jest.fn(() => {
Object.keys(mockStorage).forEach((k) => delete mockStorage[k]);
return Promise.resolve();
}),
getAllKeys: jest.fn(() => Promise.resolve(Object.keys(mockStorage))),
}));Testing a component that reads from AsyncStorage:
import AsyncStorage from '@react-native-async-storage/async-storage';
import { render, screen, waitFor } from '@testing-library/react-native';
import { UserSettings } from './UserSettings';
it('displays saved theme preference', async () => {
AsyncStorage.getItem.mockResolvedValueOnce('dark');
render(<UserSettings />);
await waitFor(() => {
expect(screen.getByTestId('theme-label')).toHaveTextContent('dark');
});
});Mocking Geolocation
// In jest.setup.js or at the top of your test file
global.navigator = global.navigator || {};
global.navigator.geolocation = {
getCurrentPosition: jest.fn((success) =>
success({
coords: {
latitude: 37.7749,
longitude: -122.4194,
accuracy: 10,
},
})
),
watchPosition: jest.fn(),
clearWatch: jest.fn(),
};Or for react-native-geolocation-service:
jest.mock('react-native-geolocation-service', () => ({
getCurrentPosition: jest.fn((success) =>
success({
coords: { latitude: 37.7749, longitude: -122.4194 },
})
),
watchPosition: jest.fn(() => 1),
clearWatch: jest.fn(),
stopObserving: jest.fn(),
}));Mocking the Camera
react-native-camera and react-native-vision-camera both require native setup. Mock them completely:
// __mocks__/react-native-vision-camera.js
const Camera = 'Camera'; // renders as a plain string component in tests
module.exports = {
Camera,
useCameraDevices: jest.fn(() => ({
back: { id: 'back', position: 'back' },
front: { id: 'front', position: 'front' },
})),
useCameraDevice: jest.fn((position) => ({
id: position,
position,
})),
useCodeScanner: jest.fn(() => ({
codeScanner: {},
})),
};Mocking Push Notifications
jest.mock('@notifee/react-native', () => ({
requestPermission: jest.fn().mockResolvedValue({ authorizationStatus: 1 }),
displayNotification: jest.fn().mockResolvedValue('notification-id'),
createChannel: jest.fn().mockResolvedValue('channel-id'),
onForegroundEvent: jest.fn(() => jest.fn()), // returns unsubscribe function
onBackgroundEvent: jest.fn(),
EventType: {
DISMISSED: 0,
PRESS: 1,
},
}));Mocking Biometrics
jest.mock('react-native-biometrics', () => {
const RNBiometrics = jest.fn().mockImplementation(() => ({
isSensorAvailable: jest.fn().mockResolvedValue({
available: true,
biometryType: 'FaceID',
}),
simplePrompt: jest.fn().mockResolvedValue({
success: true,
}),
}));
return { default: RNBiometrics };
});Platform-Specific Mocking
Sometimes you need different behavior on iOS vs Android:
import { Platform } from 'react-native';
describe('PlatformSpecificComponent', () => {
it('shows iOS-specific UI on iOS', () => {
Platform.OS = 'ios';
const { getByTestId } = render(<PlatformSpecificComponent />);
expect(getByTestId('ios-button')).toBeOnTheScreen();
});
it('shows Android-specific UI on Android', () => {
Platform.OS = 'android';
const { getByTestId } = render(<PlatformSpecificComponent />);
expect(getByTestId('android-button')).toBeOnTheScreen();
});
});Or in beforeEach:
describe('on Android', () => {
const originalOS = Platform.OS;
beforeEach(() => {
Platform.OS = 'android';
});
afterEach(() => {
Platform.OS = originalOS;
});
it('requests Android-specific permissions', async () => {
// ...
});
});Mocking Timers
For code that uses setTimeout, setInterval, or Date.now():
describe('AutoLogout', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('logs out after 30 minutes of inactivity', () => {
const onLogout = jest.fn();
render(<AutoLogout onLogout={onLogout} timeout={30 * 60 * 1000} />);
// Advance time by 30 minutes
jest.advanceTimersByTime(30 * 60 * 1000);
expect(onLogout).toHaveBeenCalledTimes(1);
});
});Verifying Mock Calls
Once you've mocked a module, verify it was called correctly:
it('saves user data to AsyncStorage on login', async () => {
const { getByTestId } = render(<LoginScreen />);
fireEvent.changeText(getByTestId('email-input'), 'user@example.com');
fireEvent.press(getByTestId('login-button'));
await waitFor(() => {
expect(AsyncStorage.setItem).toHaveBeenCalledWith(
'user_email',
'user@example.com'
);
});
});Resetting Mocks Between Tests
Mocks accumulate calls between tests unless reset. Add to jest.setup.js:
beforeEach(() => {
jest.clearAllMocks();
});Or per-mock:
afterEach(() => {
AsyncStorage.setItem.mockClear();
AsyncStorage.getItem.mockClear();
});clearAllMocks clears call history but keeps the mock implementation. resetAllMocks also removes the implementation. restoreAllMocks restores the original implementation (only works with jest.spyOn).
A Real-World Pattern: Centralizing Mocks
For large projects, maintain a __mocks__ directory at the project root:
__mocks__/
@react-native-async-storage/
async-storage.js
react-native-camera.js
react-native-geolocation-service.js
react-native-biometrics.jsEach file exports the mock implementation. Jest automatically uses them for matching imports. This keeps your test files clean and ensures consistent mock behavior across the entire test suite.
The discipline of maintaining accurate mocks pays off: when the real module's API changes (a new version of a library changes method signatures, for example), your mocks will diverge and tests will catch it.