blog / testing · react native · mobile

Mocking React Native 0.61 modules with Jest

tania@tany4 ~ % stat mocking-react-native-0.61-modules-with-jest.md
published · 6 min read

TL;DR: React Native 0.61 removed Haste, which broke a lot of Jest mocks. Here's why filename-based mocks stopped working and how I mocked react-native modules instead.

Why mocks broke in React Native 0.61

A few months after React Native 0.61 came out, I tried to run our tests and they failed.

Before 0.61, React Native used Haste to map module files by filename, so you could mock a module by its name:

jest.mock("Button", () => {});

React Native 0.61 dropped Haste, so those mocks stopped working and we got errors like:

Cannot find module 'Button'

Two ways to mock modules

The React team recommends two approaches.

1. Specify the full path:

jest.mock("react-native/Libraries/Components/Button", () => {});

This works for single modules, but the tests then depend on internal paths that can change between versions. I also couldn't get it to work in every case.

2. Mock the react-native interface

You mock react-native itself in your setup.js:

import * as ReactNative from "react-native";

jest.doMock("react-native", () => {
  return Object.setPrototypeOf(
    {
      Button: "Button",
      LayoutAnimation: {
        ...ReactNative.LayoutAnimation,
        configureNext: jest.fn(),
      },
      Platform: {
        ...ReactNative.Platform,
        OS: "ios",
        Version: 123,
        isTesting: true,
        select: (objs) => objs["ios"],
      },
      NativeModules: {
        ...ReactNative.NativeModules,
        Override: { great: "success" },
      },
    },
    ReactNative
  );
});

I had issues with this approach too. When I tried to access Platform.OS I'd get:

Couldn't read OS of undefined

What worked

I mocked react-native manually in a tests/__mocks__/react-native.js file:

import * as ReactNative from "react-native";

export const alert = jest.fn();
export const Alert = { alert };

export const dimensionWidth = 100;
export const Dimensions = {
  get: jest.fn().mockReturnValue({ width: dimensionWidth, height: 100 }),
};

export const Image = "Image";

export const keyboardDismiss = jest.fn();
export const Keyboard = { dismiss: keyboardDismiss };

export const Platform = {
  ...ReactNative.Platform,
  OS: "ios",
  Version: 123,
  isTesting: true,
  select: (objs) => objs["ios"],
};

export default Object.setPrototypeOf(
  { Alert, Dimensions, Image, Keyboard, Platform },
  ReactNative
);

I added exports for every react-native module imported in tested files, one by one. For anything that needed a mock function, I exported the mock separately so I could assert on it:

import { alert } from "react-native";

it("showAlert() calls Alert.alert", () => {
  showAlert();
  expect(alert).toHaveBeenCalled();
});

Mocking platform detection

I also needed to test platform-specific code for iOS, Android and web.

Before 0.61 we'd been using this:

export const mockPlatform = (OS) => {
  jest.resetModules();
  jest.doMock("Platform", () => ({ OS, select: (objs) => objs[OS] }));
};

This also stopped working after 0.61. Instead, I set Platform.OS directly in the test:

import { Platform } from "react-native";

it("renders Element if Android", () => {
  Platform.OS = "android";
  renderIfAndroid();
  expect(wrapper.find(Element).exists()).toBe(true);
});

With this setup we could mock everything we needed and finish the 0.61 upgrade.

I also gave two talks on React Native in production:

Written by Tania Papazafeiropoulou, Senior Front-End & Mobile Engineer.