Mocking Native Modules in React Native Tests

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 undefined

The 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-mock

In 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.js

Each 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.

Read more