Mocking Native Modules in React Native Tests

Mocking Native Modules in React Native Tests

A deep dive into mocking native modules in React Native Jest tests, covering inline jest.mock patterns, the mocks directory, NativeEventEmitter simulation, and writing robust manual mocks for complex native APIs like Camera, Location, and Bluetooth.

Native modules are the bridge between JavaScript and platform APIs like the camera, GPS, push notifications, and Bluetooth. They're essential to most production React Native apps — and they're completely unavailable in Jest's Node.js environment. Your tests will fail with NativeModule.X is null the moment you import code that touches native.

This guide shows every pattern for taming native modules in tests.

Why Native Modules Fail in Jest

When you run Jest, there's no iOS runtime, no Android JVM, no native bridge. Any NativeModules.Camera or NativeModules.Geolocation reference resolves to null or undefined. Even third-party packages that wrap native modules (like react-native-camera or @react-native-community/geolocation) will throw during import if their native side isn't available.

The solution: replace the native implementation with a JavaScript double before the test runs.

Pattern 1: Inline jest.mock

The simplest approach — mock the module inline in your test file:

jest.mock('react-native-camera', () => ({
  RNCamera: {
    takePictureAsync: jest.fn().mockResolvedValue({
      uri: 'file:///mock/photo.jpg',
      width: 1920,
      height: 1080,
    }),
    Constants: {
      Type: { back: 'back', front: 'front' },
      FlashMode: { off: 'off', on: 'on', auto: 'auto' },
    },
  },
}));

import { render, screen } from '@testing-library/react-native';
import userEvent from '@testing-library/user-event';
import { CameraScreen } from '../CameraScreen';
import { RNCamera } from 'react-native-camera';

test('takes a photo on button press', async () => {
  const user = userEvent.setup();
  render(<CameraScreen />);

  await user.press(screen.getByRole('button', { name: 'Take Photo' }));

  expect(RNCamera.takePictureAsync).toHaveBeenCalledOnce();
  await screen.findByText('Photo saved!');
});

Note: jest.mock calls are hoisted to the top of the file by Babel, even if you write them after imports. This is intentional — it's why you can reference the mocked module after the mock declaration.

Pattern 2: Factory with jest.fn()

When you need to control mock behavior per test:

const mockGetCurrentPosition = jest.fn();
const mockRequestPermissions = jest.fn();

jest.mock('@react-native-community/geolocation', () => ({
  getCurrentPosition: mockGetCurrentPosition,
  requestAuthorization: mockRequestPermissions,
  watchPosition: jest.fn(() => 1), // returns watchId
  clearWatch: jest.fn(),
}));

describe('LocationPicker', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('shows coordinates after permission granted', async () => {
    mockRequestPermissions.mockResolvedValue('granted');
    mockGetCurrentPosition.mockImplementation((success) => {
      success({
        coords: { latitude: 37.7749, longitude: -122.4194, accuracy: 10 },
        timestamp: Date.now(),
      });
    });

    render(<LocationPicker />);
    await userEvent.setup().press(screen.getByText('Get Location'));

    await screen.findByText('37.7749, -122.4194');
  });

  it('shows error when permission denied', async () => {
    mockRequestPermissions.mockResolvedValue('denied');

    render(<LocationPicker />);
    await userEvent.setup().press(screen.getByText('Get Location'));

    await screen.findByText('Location permission denied');
  });
});

Pattern 3: The mocks Folder

For native modules used across many test files, inline mocks get repetitive. The __mocks__ folder lets you define a mock once and reuse it everywhere.

Automatic mock discovery

Jest looks for mocks in two places:

  1. <rootDir>/__mocks__/ — for node_modules
  2. A __mocks__ folder adjacent to the module file

For a third-party package like react-native-device-info:

project/
  __mocks__/
    react-native-device-info.ts   ← automatically used when jest.mock() called
  node_modules/
    react-native-device-info/
// __mocks__/react-native-device-info.ts
const deviceInfo = {
  getUniqueId: jest.fn().mockResolvedValue('test-device-id-123'),
  getSystemVersion: jest.fn().mockReturnValue('17.0'),
  getBrand: jest.fn().mockReturnValue('Apple'),
  getModel: jest.fn().mockReturnValue('iPhone 16'),
  isEmulator: jest.fn().mockResolvedValue(false),
  hasNotch: jest.fn().mockReturnValue(false),
  getDeviceType: jest.fn().mockReturnValue('Handset'),
};

export default deviceInfo;

Now in any test file:

jest.mock('react-native-device-info');

// The mock from __mocks__/react-native-device-info.ts is automatically used
import DeviceInfo from 'react-native-device-info';

test('shows device info', async () => {
  render(<DeviceInfoScreen />);
  await screen.findByText('iPhone 16');
  expect(DeviceInfo.getModel).toHaveBeenCalled();
});

Overriding for specific tests

jest.mock('react-native-device-info');
import DeviceInfo from 'react-native-device-info';

test('shows emulator warning', async () => {
  // Override just for this test
  (DeviceInfo.isEmulator as jest.Mock).mockResolvedValueOnce(true);

  render(<DeviceInfoScreen />);
  await screen.findByText('Running on emulator');
});

Pattern 4: Mocking NativeEventEmitter

Many native modules emit events — push notifications, Bluetooth connection changes, location updates. The NativeEventEmitter class is the bridge.

Manual event emitter mock

// __mocks__/NativeEventEmitterMock.ts
type Listener = (...args: any[]) => void;

export class MockNativeEventEmitter {
  private listeners: Map<string, Listener[]> = new Map();

  addListener(event: string, listener: Listener) {
    const existing = this.listeners.get(event) ?? [];
    this.listeners.set(event, [...existing, listener]);
    return {
      remove: () => this.removeListener(event, listener),
    };
  }

  removeListener(event: string, listener: Listener) {
    const existing = this.listeners.get(event) ?? [];
    this.listeners.set(event, existing.filter(l => l !== listener));
  }

  emit(event: string, ...args: any[]) {
    const listeners = this.listeners.get(event) ?? [];
    listeners.forEach(l => l(...args));
  }

  removeAllListeners(event?: string) {
    if (event) {
      this.listeners.delete(event);
    } else {
      this.listeners.clear();
    }
  }
}

Using it to test event-driven components

jest.mock('react-native', () => {
  const RN = jest.requireActual('react-native');
  const { MockNativeEventEmitter } = require('./__mocks__/NativeEventEmitterMock');
  return {
    ...RN,
    NativeEventEmitter: MockNativeEventEmitter,
    NativeModules: {
      ...RN.NativeModules,
      BluetoothManager: {
        startScan: jest.fn(),
        stopScan: jest.fn(),
        connect: jest.fn(),
      },
    },
  };
});

import { NativeEventEmitter, NativeModules } from 'react-native';

let emitter: InstanceType<typeof NativeEventEmitter>;

beforeEach(() => {
  emitter = new NativeEventEmitter(NativeModules.BluetoothManager as any);
});

test('shows device when Bluetooth scan finds one', async () => {
  render(<BluetoothScanner />);

  // Simulate a native event
  act(() => {
    (emitter as any).emit('DeviceFound', {
      id: 'AA:BB:CC:DD:EE:FF',
      name: 'AirPods Pro',
      rssi: -65,
    });
  });

  await screen.findByText('AirPods Pro');
});

test('shows disconnected state on disconnect event', async () => {
  render(<BluetoothScanner connectedDeviceId="AA:BB:CC:DD:EE:FF" />);

  act(() => {
    (emitter as any).emit('DeviceDisconnected', { id: 'AA:BB:CC:DD:EE:FF' });
  });

  await screen.findByText('Disconnected');
});

Pattern 5: Mocking react-native Internals

Sometimes you need to mock React Native's own modules — Linking, Alert, Clipboard, AppState:

jest.mock('react-native/Libraries/Linking/Linking', () => ({
  openURL: jest.fn().mockResolvedValue(null),
  canOpenURL: jest.fn().mockResolvedValue(true),
  getInitialURL: jest.fn().mockResolvedValue(null),
  addEventListener: jest.fn(() => ({ remove: jest.fn() })),
}));

For Alert:

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

import { Alert } from 'react-native';

test('shows confirmation alert before deleting', async () => {
  const user = userEvent.setup();
  render(<DeleteButton itemId="123" />);

  await user.press(screen.getByText('Delete'));

  expect(Alert.alert).toHaveBeenCalledWith(
    'Confirm Delete',
    'Are you sure?',
    expect.arrayContaining([
      expect.objectContaining({ text: 'Cancel' }),
      expect.objectContaining({ text: 'Delete' }),
    ])
  );
});

test('deletes item when confirmed', async () => {
  const mockDelete = jest.fn();

  // Make Alert call the "Delete" button's onPress
  (Alert.alert as jest.Mock).mockImplementation((title, message, buttons) => {
    const deleteButton = buttons.find((b: any) => b.text === 'Delete');
    deleteButton?.onPress?.();
  });

  const user = userEvent.setup();
  render(<DeleteButton itemId="123" onDelete={mockDelete} />);

  await user.press(screen.getByText('Delete'));

  expect(mockDelete).toHaveBeenCalledWith('123');
});

Pattern 6: Mocking Complex Packages

Some packages like react-native-permissions require more elaborate mocking:

// __mocks__/react-native-permissions.ts
import { PERMISSIONS, RESULTS } from 'react-native-permissions';

const mockPermissions: Record<string, string> = {};

export { PERMISSIONS, RESULTS };

export const check = jest.fn((permission: string) =>
  Promise.resolve(mockPermissions[permission] ?? RESULTS.DENIED)
);

export const request = jest.fn((permission: string) =>
  Promise.resolve(mockPermissions[permission] ?? RESULTS.DENIED)
);

export const checkMultiple = jest.fn((permissions: string[]) =>
  Promise.resolve(
    Object.fromEntries(
      permissions.map(p => [p, mockPermissions[p] ?? RESULTS.DENIED])
    )
  )
);

export const requestMultiple = jest.fn((permissions: string[]) =>
  Promise.resolve(
    Object.fromEntries(
      permissions.map(p => [p, mockPermissions[p] ?? RESULTS.DENIED])
    )
  )
);

// Helper for test setup
export const __setPermission = (permission: string, result: string) => {
  mockPermissions[permission] = result;
};

export const __resetPermissions = () => {
  Object.keys(mockPermissions).forEach(k => delete mockPermissions[k]);
};
// In tests
import { PERMISSIONS, RESULTS, __setPermission, __resetPermissions } from 'react-native-permissions';

jest.mock('react-native-permissions');

beforeEach(() => __resetPermissions());

test('camera screen works with camera permission', async () => {
  __setPermission(PERMISSIONS.IOS.CAMERA, RESULTS.GRANTED);

  render(<CameraScreen />);
  await screen.findByTestId('camera-view');
});

test('shows permission prompt when camera denied', async () => {
  __setPermission(PERMISSIONS.IOS.CAMERA, RESULTS.DENIED);

  render(<CameraScreen />);
  await screen.findByText('Camera access required');
});

Pattern 7: Global Mock Setup

For modules mocked in every test, configure them in Jest's setup file:

// jest.setup.ts
import mockAsyncStorage from '@react-native-async-storage/async-storage/jest/async-storage-mock';

jest.mock('@react-native-async-storage/async-storage', () => mockAsyncStorage);

jest.mock('react-native-reanimated', () =>
  require('react-native-reanimated/mock')
);

jest.mock('@react-native-firebase/analytics', () => ({
  default: () => ({
    logEvent: jest.fn(),
    setUserId: jest.fn(),
    setUserProperties: jest.fn(),
    logScreenView: jest.fn(),
  }),
}));
// jest.config.js
module.exports = {
  preset: 'react-native',
  setupFilesAfterFramework: [
    '@testing-library/jest-native/extend-expect',
    './jest.setup.ts',
  ],
};

Verifying Mock Calls

Mocks are only useful if you assert on them correctly:

import { RNCamera } from 'react-native-camera';

test('calls camera with correct options', async () => {
  render(<PhotoCapture quality="high" flash="auto" />);
  await userEvent.setup().press(screen.getByText('Capture'));

  expect(RNCamera.takePictureAsync).toHaveBeenCalledWith({
    quality: 1.0,
    fixOrientation: true,
    flashMode: 'auto',
  });
});

test('does not call camera before user taps', () => {
  render(<PhotoCapture />);
  expect(RNCamera.takePictureAsync).not.toHaveBeenCalled();
});

Common Mistakes

Don't mock react-native wholesale. jest.mock('react-native') without jest.requireActual will remove all the real RN APIs. Always spread the actual module:

// WRONG
jest.mock('react-native', () => ({
  Alert: { alert: jest.fn() },
}));

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

Don't forget to clear mocks between tests. Use clearMocks: true in Jest config or explicit jest.clearAllMocks() in beforeEach.

Don't mock what you don't need to. If a component only touches Text and View, no mocking is required. Mock only the native boundary.

Summary

Native module mocking follows a clear hierarchy: inline jest.mock for one-off cases, __mocks__ folder for shared mocks, jest.requireActual to preserve real RN internals, and a custom NativeEventEmitter implementation for event-driven modules. Get your global setup file right and most tests will just work — reserving per-test overrides for edge cases like permission states and error conditions.

Read more

Start now free