React Navigation 5: Unit Testing Components

The release of React Navigation 5 drastically changed how navigation is structured for React Native projects. One major change with this release was the introduction of useNavigation.

This React Hook makes it easy to access the navigation prop from child components without needing to pass it down through props, but what does this mean for unit testing components that use this hook?

The Problem

If you’ve tried testing a component that uses the useNavigation hook, you’ve likely seen this error message:

Couldn’t find a navigation object. Is your component inside a screen in a navigator?

This happens because the useNavigation hook needs to retrieve the navigation prop from a parent Screen component, and you’re most likely rendering the component in isolation. This leads to a test failure since the hook is not able to retrieve the navigation prop.

Don’t worry. There are a handful of ways to handle this.

Solution #1: Wrapping with TestNavigationContainer

The error message tells us what the problem is. If we wrapped our component in a Screen while we were testing, it we would be able to access the navigation prop, and our test would pass.

You could create a MockedNavigator component as described in this post by Daria Rucka:

import React from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createStackNavigator } from "@react-navigation/stack";

const Stack = createStackNavigator();
const MockedNavigator = ({ component, params = {} }) => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="MockedScreen"
          component={ component }
          initialParams={ params }
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default MockedNavigator;

The benefit of this approach is that it provides an actual navigation prop to the component under test. Unfortunately, it also requires rendering each component that uses the useNavigation hook in a MockedNavigator component for every unit test. This approach also lacks the ability to assert the usage of the navigation prop in the component.

Solution #2: Mocking useNavigation in Jest Setup File

Another option would be to mock the @react-navigation/native package entirely. This would remove the need to wrap the component under test in a MockedNavigator since the useNavigation hook wouldn’t actually try to retrieve a navigation prop from a parent Screen component.

Jest mocks can be configured in a few different ways, but I found the following steps to be fairly straightforward (assuming you’ve already followed the Testing with Jest section of the React Navigation docs).

The only change that’s required is to add this to the Jest setup file:


jest.mock("@react-navigation/native", () => {
  const actualNav = jest.requireActual("@react-navigation/native");
  return {
    ...actualNav,
    useNavigation: () => ({
      navigate: jest.fn(),
      dispatch: jest.fn(),
    }),
  };
});

With this mock in place, anywhere that useNavigation is imported from @react-navigation/native will now receive the mocked version.

This works and eliminates the need to wrap each component with a MockedNavigator while testing, but we’re still missing the ability to assert the usage of the navigation prop in the component. Let’s change that.

Solution #3: Mocking useNavigation per Test File

Both of the previous solutions lacked the ability to assert how the navigation prop is used in the component under test. What if you wanted to write a test that verifies a particular DrawerAction is dispatched using the navigation prop when a button is pressed? We could write an end-to-end test for this with something like Detox, but I would prefer a quicker solution that doesn’t require nearly as much setup.

In order to assert the usage of the navigation prop, we will need to pass in our own mock functions for the navigate or dispatch functions. We were already using jest.fn() in the Jest setup file, but we didn’t have a way to reference those functions to make assertions on.

With the React Navigation mock in the test file, we can initialize a variable to hold a reference to the jest.fn() that we return from the mocked useNavigation. Now assertions can be made about how many times the mock function was called, what parameters it was called with, and so much more.

Here’s an example testing a MenuButton component that should dispatch the toggleDrawer action when pressed:

import React from "react";
import MenuButton from "./MenuButton";
import { render, fireEvent } from "@testing-library/react-native";
import { DrawerActions } from "@react-navigation/native";

const mockedDispatch = jest.fn();

// Mocks like this need to be configured at the top level 
// of the test file, they can't be setup inside your tests.
jest.mock("@react-navigation/native", () => {
  const actualNav = jest.requireActual("@react-navigation/native");
  return {
    ...actualNav,
    useNavigation: () => ({
      navigate: jest.fn(),
      dispatch: mockedDispatch,
    }),
  };
});

describe("MenuButton", () => {
  beforeEach(() => {
    // Alternatively, set "clearMocks" in your Jest config to "true"
    mockedDispatch.mockClear();
  });

  it("toggles navigation drawer on press", () => {
    const { getByTestId } = render(<MenuButton />);
    const button = getByTestId("menu-button");
    fireEvent.press(button);

    expect(mockedDispatch).toHaveBeenCalledTimes(1);
    expect(mockedDispatch).toHaveBeenCalledWith(DrawerActions.toggleDrawer());
  });
});

As is the case with any JavaScript project, there are plenty of ways to achieve the same result. The trick is knowing which techniques to apply at the appropriate time. I prefer the level of detail that the third solution provides, but that doesn’t mean that the other solutions aren’t also viable. Try them out on your own project and see which one works best with your workflow.

I hope this post has taught you a thing or two about testing your components that use React Navigation and about mocking JavaScript libraries in general.

Conversation
  • Lee says:

    Good article. However this feels like it’s testing implementation details rather than behaviour.

  • Comments are closed.