Updated September 25, 2020

Setup Jest Tests with React Navigation

So you're using React Navigation and you want to write tests for your app. Maybe you're running into problems or you're just curious how the workflow works out. Let's cover it.

Say we've got the following app. It's composed of 2 stack navigators and a total of 3 screens.

App.js

import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { StyleSheet, Text, View, Button } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';

export const Screen1 = ({ navigation }) => (
  <View style={styles.container}>
    <Text>Screen 1</Text>
    <Button title="Go to Screen 2" onPress={() => navigation.push('Screen2')} />
    <StatusBar style="auto" />
  </View>
);

export const Screen2 = () => (
  <View style={styles.container}>
    <Text>Screen 2</Text>
    <StatusBar style="auto" />
  </View>
);

const Stack = createStackNavigator();
export const AppStack = () => (
  <Stack.Navigator>
    <Stack.Screen name="Screen1" component={Screen1} />
    <Stack.Screen name="Screen2" component={Screen2} />
  </Stack.Navigator>
);

const SignIn = () => (
  <View style={styles.container}>
    <Text>Sign In</Text>
    <StatusBar style="auto" />
  </View>
);

const AuthStack = () => (
  <Stack.Navigator>
    <Stack.Screen name="SignIn" component={SignIn} />
  </Stack.Navigator>
);

export default ({ isLoggedIn = true }) => (
  <NavigationContainer>
    {isLoggedIn ? <AppStack /> : <AuthStack />}
  </NavigationContainer>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

Testing a Screen

Okay, let's say we want to write a test for Screen1. I want to check that the "Screen 1" text is renderer and that when the button is pressed we navigate to the next screen. I'll be using @testing-library/react-native to interact with my app. Highly recommend it.

App.test.js

import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';

import App, { Screen1, AppStack } from './App';

describe('Screen 1', () => {
  it('navigates on button press', () => {
    const push = jest.fn();
    const { getByText } = render(<Screen1 navigation={{ push }} />);
    fireEvent.press(getByText('Go to Screen 2'));
    expect(push).toHaveBeenCalledWith('Screen2');
  });
});

Pretty standard test, right? The only React Navigation specific thing we had to do here is pass a navigation prop to our component when we render it. Within that object we had to pass a mocked push function to run an assertion against.

When testing a screen that React Navigation renders you may need to render the navigation and the route properties.

You can learn what properties you may need to mock by reading the docs:

Testing a Navigator

Next up is testing a navigator itself. An example of why you would want to do this is ensuring that the right screen is being shown given the state.

import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';

import App, { Screen1, AppStack } from './App';

// ...

describe('AppStack', () => {
  it('renders the correct screen', async () => {
    const { getByText } = render(
      <NavigationContainer>
        <AppStack />
      </NavigationContainer>
    );
    await waitFor(() => getByText('Screen 1'));
  });
});

All we've done here is rendered our stack (because remember that all navigators in React Navigation are, at the end, just a really smart components). The only special thing we had to do is wrap that navigator in the NavigationContainer because that provides necessary information for it to render.

Now, if you ran this test you may be seeing the following in your test output.

console.warn
    Animated: `useNativeDriver` is not supported because the native animated module is missing.
    Falling back to JS-based animation. To resolve this, add `RCTAnimation` module to this app,
    or remove `useNativeDriver`. Make sure to run `pod install` first.
    Read more about autolinking: https://github.com/react-native-community/cli/blob/master/docs/autolinking.md

Trying to decipher this can be somewhat tricky but, if you check out the docs from React Navigation, they explain this.

Remember how I said React Navigation navigators are just really smart components? Well they leverage other smart tools to do that. Smart tools that we need to mock in our tests because they don't really work in the test environment.

To do so I'm going to create a testing setup file.

testing/jest-setup.js

import 'react-native-gesture-handler/jestSetup';

jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper');

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

  // The mock for `call` immediately calls the callback which is incorrect
  // So we override it with a no-op
  Reanimated.default.call = () => {};

  return Reanimated;
});

This is all outlined in the React Navigation testing documentation so if you're still seeing some warnings make sure that you check the docs for any updates to modules you need to mock.

The final thing you need to do is tell Jest about this setup file.

package.json

{
  // ...
  "jest": {
    "preset": "jest-expo",
    "transformIgnorePatterns": [
      "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*)"
    ],
    "setupFiles": ["<rootDir>/testing/jest-setup.js"]
  }
}

To my Jest config in package.json I've added a setupFiles array which points to the setup file I just created. This will be called before our tests are run so the libraries are properly mocked and tests run without warnings.

PASS  ./App.test.js
Screen 1
  ✓ navigates on button press (48ms)
AppStack
  ✓ renders the correct screen (44ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.84s, estimated 2s
Ran all test suites related to changed files.

Testing the Full App

Getting pretty wild now but let's say we want to write a test that renders our entire app...

Well it's just a component so render it!

App.test.js

import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';

import App, { Screen1, AppStack } from './App';

// ...

describe('App', () => {
  it('renders app stack', async () => {
    const { getByText } = render(<App />);
    await waitFor(() => getByText('Screen 1'));
  });
});

We should add an extra test - if we pass isLoggedIn={false} to our app let's make sure we see our sign in screen instead of Screen 1.

App.test.js

import React from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';

import App, { Screen1, AppStack } from './App';

// ...

describe('App', () => {
  it('renders app stack', async () => {
    const { getByText } = render(<App />);
    await waitFor(() => getByText('Screen 1'));
  });

  it('renders auth stack', async () => {
    const { getByText } = render(<App isLoggedIn={false} />);
    await waitFor(() => getByText('Sign In'));
  });
});

Conclusion

Testing screen/components that interact with React Navigation is no big deal if you remember that React Navigation navigators are just components and you can test them just like any other.

They're really smart components so you'll have to mock a few of the libraries they use but the docs will tell you which ones.

Testing with Jest

You can find the code used in this example on Github.

React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our courses and our private Slack community.

Learn More