Expo UI and Router for iOSExpo SDK 57 and later, iOS only
Expo Router

Testing and troubleshooting

Learn how to create integration tests for your app when using Expo Router.

Expo Router relies on your file system, which can present challenges when setting up mocks for integration tests. Expo Router's submodule, expo-router/testing-library, is a set of testing utilities built on top of the popular @testing-library/react-native and allows you to quickly create in-memory Expo Router apps that are pre-configured for testing.

Configuration

Before you proceed, ensure you have set up jest-expo according to the Unit testing with Jest and @testing-library/react-native in your project.

Note: When using Expo Router, do not put your test files inside the app directory. All files inside your app directory must be either routes or layout files. Instead, use the tests directory or a separate directory. This approach is explained in Unit testing with Jest.

renderRouter

renderRouter extends the functionality of render to simplify testing with Expo Router. It returns the same query object as render, and is compatible with screen, allowing you to use the standard query API to locate components.

renderRouter accepts the same options as render and introduces an additional option initialUrl, which sets an initial route for simulating deep-linking.

Inline file system

renderRouter(mock: Record<string, ReactComponent>, options: RenderOptions)

renderRouter can provide inline-mocking of a file system by passing an object to this function as the first parameter. The keys of the object are the mock filesystem paths. Do not use leading relative (./) or absolute (/) notation when defining these paths and exclude file extension.

import { renderRouter, screen } from 'expo-router/testing-library';
import { View } from 'react-native';

it('my-test', async () => {
  const MockComponent = jest.fn(() => <View />);

  renderRouter(
    {
      index: MockComponent,
      'directory/a': MockComponent,
      '(group)/b': MockComponent,
    },
    {
      initialUrl: '/directory/a',
    }
  );

  expect(screen).toHavePathname('/directory/a');
});

Inline file system with `null` components

renderRouter(mock: string[], options: RenderOptions)

Providing an array of strings to renderRouter will create an inline mock filesystem with null components ({ default: () => null }). This is useful for testing scenarios where you do not need to test the output of a route.

import { renderRouter, screen } from 'expo-router/testing-library';

it('my-test', async () => {
  renderRouter(['index', 'directory/a', '(group)/b'], {
    initialUrl: '/directory/a',
  });

  expect(screen).toHavePathname('/directory/a');
});

Path to fixture

renderRouter(fixturePath: string, options: RenderOptions)

renderRouter can accept a directory path to mock an existing fixture. Ensure that the provided path is relative to the current test file.

import { renderRouter } from 'expo-router/testing-library';
import { View } from 'react-native';

it('my-test', async () => {
  const MockComponent = jest.fn(() => <View />);
  renderRouter('./my-test-fixture');
});

Path to the fixture with overrides

renderRouter({ appDir: string, overrides: Record<string, ReactComponent>}, options: RenderOptions)

For more intricate testing scenarios, renderRouter can leverage both directory path and inline-mocking methods simultaneously.

  • The appDir parameter takes a string representing a pathname to a directory.
  • The overrides parameter is an inline mock that can be used to override specific paths within the appDir.
  • This combination allows for fine-tuned control over the mock environment.
import { renderRouter } from 'expo-router/testing-library';
import { View } from 'react-native';

it('my-test', async () => {
  const MockAuthLayout = jest.fn(() => <View />);
  renderRouter({
    appDir: './my-test-fixture',
    overrides: {
      'directory/(auth)/_layout': MockAuthLayout,
    },
  });
});

Jest matchers

The following matches have been added to expect and can be used to assert values on screen.

toHavePathname()

Assert the current pathname against a given string. The matcher uses the value of the usePathname hook on the current screen.

expect(screen).toHavePathname('/my-router');

toHavePathnameWithParams()

Assert the current pathname, including URL parameters, against a given string. This is useful to assert the appearance of URL in a web browser.

expect(screen).toHavePathnameWithParams('/my-router?hello=world');

toHaveSegments()

Assert the current segments against an array of strings. The matcher uses the value of the useSegments hook on the current screen.

expect(screen).toHaveSegments(['[id]']);

useLocalSearchParams()

Assert the current local URL parameters against an object. The matcher uses the value of the useLocalSearchParams hook on the current screen.

expect(screen).useLocalSearchParams({ first: 'abc' });

useGlobalSearchParams()

Assert the current global URL parameters against an object. The matcher uses the value of the useGlobalSearchParams hook on the current screen.

expect(screen).useGlobalSearchParams({ first: 'abc' });

toHaveRouterState()

An advanced matcher that asserts the current router state against an object.

expect(screen).toHaveRouterState({
  routes: [{ name: 'index', path: '/' }],
});

Troubleshooting

Missing files or source maps in React Native DevTools

This can happen if your Chrome DevTools has exclusions within its ignore list. To fix the issue, use React Native DevTools:

  1. Start the React Native DevTools by pressing J from the development server running in the terminal window
  2. Open Settings by clicking the gear icon
  3. Under Extensions, click Restore defaults and reload
  4. Open Settings again, and go to Ignore List tab
  5. Uncheck any exclusions for /node_modules/

EXPO_ROUTER_APP_ROOT not defined

If process.env.EXPO_ROUTER_APP_ROOT is not defined you'll see the following error:

Invalid call at line 11: process.env.EXPO_ROUTER_APP_ROOT First argument of require.context should be a string.

This can happen when the Babel plugin expo-router/babel is not used in the project babel.config.js. You can try clearing the cache with:

bun expo start --clear

Alternatively, you can circumvent this issue by creating an index.js file in the root of your project with the following contents:

import { registerRootComponent } from 'expo';
import { ExpoRoot } from 'expo-router';

// Must be exported or Fast Refresh won't update the context
export function App() {
  const ctx = require.context('./app');
  return <ExpoRoot context={ctx} />;
}

registerRootComponent(App);

Then, update your app's main entry point in package.json:

{
  "main": "index.js"
  ... 
}

Do not use this to change the root directory (app) as it won't account for usage in any other places.

require.context not enabled

This can happen when using a custom version of @expo/metro-config that does not enable context modules.

  • Expo Router requires the project metro.config.js to use expo-router/metro as the default configuration.
  • Delete the metro.config.js, or extend expo/metro-config.
  • See Customizing metro for more information.

Missing back button

If you set up a modal or another screen that is expected to have a back button, then you'll need to add unstable_settings to the route's layout to ensure the initial route is configured. Initial routes are somewhat unique to mobile apps and fit awkwardly in the system — improvements pending.

export const unstable_settings = {
  initialRouteName: 'index',
};

Sources

The original Expo docs for this page:

On this page