12 Nov 2026 · 6 min read

jest-expo setup, Expo Router tests and fixes

Set up jest-expo in an Expo app: the preset, React Native Testing Library, Expo Router screen tests, native mocks, CI, and where E2E starts.

Jest runs in Node, and an Expo screen imports native modules that do not exist there. An Expo app needs three things for tests: a Jest preset that mocks the native side, a renderer for components, and a helper for screens that use Expo Router. This guide sets up all three, then shows where unit tests stop and end-to-end tests on a device begin.

Four test layers, from fast to realFour boxes from left to right: unit tests with jest-expo, component tests with React Native Testing Library, router tests with renderRouter, and Maestro end-to-end flows on a dev build on a device.Unitstores, helpersjest-expoComponentrender, queriesfireEventRouterrenderRouterroutes, paramsE2EMaestro flowsdev build, devicemany, fast, every pushfew, slow, on a real build

What jest-expo is

jest-expo is a Jest preset. Expo's guide describes it as a preset that mocks the native part of the Expo SDK and handles most of the configuration an Expo project needs. You add it once, in your Jest config, and the rest of your tests stay plain Jest.

1. Install the packages

npx expo install jest-expo jest @types/jest --dev

Run this from the project root. In a Bun project, use bun expo install with the same packages, as Expo's guide shows. On SDK 58 and later, jest-expo needs Jest 30, so upgrade jest and @types/jest together with it. Then add React Native Testing Library the same way:

npx expo install @testing-library/react-native --dev

In a TypeScript project, add "jest" to the types array in tsconfig.json so the Jest globals type-check:

{
  "compilerOptions": {
    "types": ["jest"]
  }
}

2. Point Jest at the preset

Expo's guide puts the config in package.json, with a test script. This is the Bun variant of the transformIgnorePatterns from the guide, copied as written:

{
  "scripts": {
    "test": "jest"
  },
  "jest": {
    "preset": "jest-expo",
    "transformIgnorePatterns": [
      "node_modules/(?!(.bun|(jest-)?react-native|@react-native(-community)?|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg))"
    ]
  }
}

The pattern names the packages Jest should transpile. When a dependency fails with a syntax error, add it to the list (the errors section below shows how). ReadyNative keeps the same two keys in jest.config.js, with a longer list for the UI libraries it ships, and an @/ path mapping for src.

3. Test a component

React Native Testing Library renders a component and gives you queries such as getByText, plus fireEvent to press it. Since version 14, render returns a promise, so await it. This test from ReadyNative's UI kit checks that a tap reaches onPress:

import { describe, expect, it, jest } from "@jest/globals";
import { fireEvent, render, screen } from "@testing-library/react-native";

import { Button, ThemeProvider } from "@/ui";

describe("@/ui Button", () => {
  it("calls onPress when tapped", async () => {
    const onPress = jest.fn();
    await render(
      <ThemeProvider>
        <Button onPress={onPress}>Tap</Button>
      </ThemeProvider>
    );

    fireEvent.press(screen.getByText("Tap"));
    expect(onPress).toHaveBeenCalledTimes(1);
  });
});

The kit's tests wrap components in ThemeProvider, as its module docs describe for screens. Wrap your screen in whatever providers it reads, such as a TanStack Query client.

4. Mock the native modules you use

jest-expo mocks the native side of the SDK. Your own native dependencies may need a mock too. Put it in a setup file that Jest loads before every test file, through setupFilesAfterEnv. ReadyNative's jest.setup.ts does this for haptics, symbols and storage:

import { jest } from "@jest/globals";

// Trimmed: the real file also covers reanimated and more of the storage API.
jest.mock("expo-haptics", () => ({
  impactAsync: jest.fn(() => Promise.resolve()),
  ImpactFeedbackStyle: { Light: "light", Medium: "medium", Heavy: "heavy" },
}));

jest.mock("expo-symbols", () => ({ SymbolView: () => null }));

// In-memory stand-in for expo-sqlite/kv-store, so stores run without a native database.
jest.mock(
  "expo-sqlite/kv-store",
  () => {
    const store = new Map<string, string>();
    return {
      __esModule: true,
      default: {
        getItem: (key: string) => Promise.resolve(store.get(key) ?? null),
        setItem: (key: string, value: string) => {
          store.set(key, value);
          return Promise.resolve();
        },
      },
    };
  },
  { virtual: true }
);

The last mock passes virtual: true. ReadyNative uses it for a module that a setup can leave out: the storage mock still loads when another storage option is chosen. The mock only replaces the module inside tests, so the app code does not change.

5. Test Expo Router screens

Expo Router reads routes from the file system, which makes mocks awkward. The expo-router/testing-library submodule builds a small in-memory app for each test, on top of React Native Testing Library. Pass renderRouter a list of route names, then move between them with testRouter:

import { expect, it } from "@jest/globals";
import { renderRouter, screen, testRouter } from "expo-router/testing-library";

it("navigates to the profile", async () => {
  await renderRouter(["index", "profile"]);
  await testRouter.push("/profile");

  expect(screen).toHavePathname("/profile");
});

A list of names gives each route an empty screen. Pass an object of components instead when a test needs a route's output, and set initialUrl to start a test on a deep link. The matchers toHavePathname, toHaveSegments and toHaveSearchParams assert on the screen.

Check the version first. Expo's page says it covers SDK 58 and later, that expo-router/testing-library needs @testing-library/react-native 14 or newer, and that on SDK 57 it works only with version 13. Keep your router tests out of the app directory too: every file there is a route or a layout, so a test file there becomes a screen.

Errors you will meet

Running in CI

ReadyNative's CI workflow runs on every push and pull request, on a plain ubuntu-latest runner. It installs with the lockfile frozen, then runs typecheck, lint and the tests:

bun install --frozen-lockfile
bun run typecheck
bun run lint
bun run test --ci

Jest needs no simulator, so the job runs on a Linux runner. The Jest tests run the JavaScript of your app. They do not run it on a device.

Where unit tests stop and E2E starts

A Jest test cannot show that the app launches, that a permission prompt appears, or that a native module works in a real build. Expo's unit testing guide says that for UI testing it recommends end-to-end tests instead of snapshot unit tests.

ReadyNative's end-to-end layer is Maestro. bun run test:e2e runs the flows in apps/template/.maestro/flows against a dev build installed on a simulator or emulator, with the Metro server running. The two shipped flows clear the app state, step through the onboarding slides and check the tab bar, then open Settings and switch the theme. The flows target the app's own bundle id, so Expo Go cannot run them. The CI workflow does not run them.

A simple split works well. Logic in stores and helpers gets a Jest unit test. A screen's text and taps get a component test. A route and its params get a router test. The first launch and the flows that matter most get a Maestro flow on a dev build.

Read next: Expo Go vs development build, which explains why the E2E flows need a dev build, then the Jest + Testing Library stack page and the full Jest module docs.

FAQ

Do I need jest-expo, or can I use plain Jest?

Use jest-expo. It is the preset Expo documents for this: it mocks the native part of the Expo SDK and handles most of the Jest configuration an Expo project needs.

Why does Jest fail with 'Cannot use import statement outside a module'?

A package in node_modules ships ES modules that Jest does not transform. Add the package name to transformIgnorePatterns in your Jest config, keeping the pattern from Expo's guide, then run the tests again.

Does expo-router/testing-library work on SDK 57?

Expo's page says it needs @testing-library/react-native 14 or newer, and that on SDK 57 it works only with version 13. Check the version your project resolves before you write router tests.

Should Jest tests replace end-to-end tests?

No. Jest runs your JavaScript on your machine, not the app on a device. The ReadyNative template ships Maestro flows for the device side, run with bun run test:e2e against a dev build.

More guides