Org Skills

react-native-testing

Design a React Native / Expo test strategy and write the tests — Jest with the jest-expo preset, React Native Testing Library queries and userEvent, mocking Expo modules, Reanimated and MMKV, faking the network with MSW, routing tests with renderRouter from expo-router/testing-library, and Maestro end-to-end flows on EAS Build.

Download .zip Raw Source
When agents use itUse this whenever the user asks how to test an Expo screen, component, hook, store or navigation flow; sets up jest-expo, jest.setup.js or a testing preset; asks about @testing-library/react-native, getByRole vs getByTestId, userEvent.press, act() warnings, "cannot find variable __DEV__", mocking expo-secure-store or useSafeAreaInsets, flaky timers, or device tests in CI. Also use it when generating tests for React Native code the user just wrote, even if they do not say the word "test". For web React use react-testing.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill react-native-testing -a github-copilot
# or with the org installer (adds .github/skills/react-native-testing):
npx -y github:AGCO-Global/org-skills add skill react-native-testing

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill react-native-testing
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill react-native-testing -g

Installs the frontend-react-native-skills plugin, which bundles all Frontend / React Native skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-react-native-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill react-native-testing -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill react-native-testing -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill react-native-testing -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill react-native-testing -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Try it — example prompts

Prompts this skill is tested against, and what a good answer includes.

  • Set up testing for our Expo SDK 57 app — we have nothing yet.

    jest-expo preset in package.json or jest.config.js @testing-library/react-native jest.setup.ts with MSW server.listen and onUnhandledRequest: 'error' react-native-reanimated/jest/resolver and setUpTests() SafeAreaProvider with initialMetrics in the render wrapper

  • Write tests for this OrdersScreen — it uses useOrders (TanStack Query) and shows an offline banner.

    lists behaviours first renders the real screen with the provider wrapper MSW handlers for the list, empty and error responses findBy queries by role, label or text asserts the offline and empty states, not just the happy path

  • My RNTL test times out on await user.press(...) after I enabled jest.useFakeTimers().

    userEvent.setup({ advanceTimers: jest.advanceTimersByTime }) jest.useRealTimers() in afterEach notes that press replays a full touch sequence and takes simulated time

  • How do I test that the (app) routes are only reachable when the user is signed in?

    renderRouter from expo-router/testing-library initialUrl pointing at the protected route expect(screen).toHavePathname('/sign-in') both directions tested: signed in and signed out

  • We need end-to-end tests on real devices in CI for sign-in and checkout.

    Maestro flows in .maestro/ with launchApp clearState and assertVisible EAS build profile with withoutCredentials, ios simulator: true, android buildType apk a small number of seeded, independent flows run on pull requests via an EAS workflow

  • Our screen calls expo-secure-store and the test fails at import.

    jest.mock('expo-secure-store') with a Map-backed fake implementing setItemAsync/getItemAsync/deleteItemAsync mock registered centrally in the setup file assert the token round-trips rather than that a function was called

Skill contents

React Native Testing

Targets: Expo SDK 54+ · jest-expo · React Native Testing Library 13 · MSW 2 · Maestro · Verified: 2026-09 against https://docs.expo.dev, https://callstack.github.io/react-native-testing-library, https://mswjs.io, https://docs.swmansion.com

A mobile test suite earns its keep by catching the bugs you cannot see in the simulator: a screen that breaks with no network, a permission denial you never handled, a form that cannot be submitted with the keyboard up. Test through the rendered screen with real hooks and a faked network; keep the device-level flows to a handful of Maestro journeys, because every one of them costs minutes of CI on real hardware.

1. Decide first

Question Default Change when
Which layer? Component test of a screen with RNTL + MSW, rendered with the real providers Pure logic in model/ or a reducer → plain Jest unit test with no React
Runner Jest with the jest-expo preset — it handles the RN transform, Expo modules and the platform extensions A non-Expo bare app → preset: 'react-native' plus the same RNTL rules
Query getByRole('button', { name: … }), getByLabelText, getByText An element genuinely has no accessible name → fix the component; getByTestId only with a comment
Interaction const user = userEvent.setup() then await user.press(...) A low-level event RNTL cannot express → fireEvent with a comment
Network MSW 2, imported from msw/native A native module (camera, biometrics) → jest.mock the Expo module
Navigation Assert what the screen renders, not that the router was called The flow is the feature (guards, redirects, deep links) → renderRouter from expo-router/testing-library
End-to-end 3–8 Maestro flows on EAS Build for the critical journeys The team already runs Detox and it is green → keep it; do not run both
Snapshots No A small serialisable value (a parsed config, a reducer output) → toMatchInlineSnapshot

2. Setup

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

Five things the configuration must have, or the suite fails before it asserts anything:

  • "preset": "jest-expo" — supplies the transform, the Expo module resolution and __DEV__.
  • "resolver": "react-native-reanimated/jest/resolver" plus require('react-native-reanimated').setUpTests() in the setup file.
  • MSW started in setupFilesAfterEnv with onUnhandledRequest: 'error', server.resetHandlers() in afterEach, using setupServer from msw/native.
  • A SafeAreaProvider with initialMetrics in the render wrapper — without it useSafeAreaInsets() never resolves and screens render nothing.
  • A QueryClient with retry: false, so an error-path test does not wait out three attempts.

RNTL's matchers (toBeOnTheScreen, toHaveTextContent) need no setup import. Full config, the wrapper and the Expo/MMKV module mocks: references/setup.md.

3. Component tests — the rules

  • Render the screen, not the pieces. Real query hooks, real form library, faked HTTP. That is what catches the wiring bugs.
  • Query the way a screen reader does. getByRole with an accessible name works because the component set accessibilityRole and accessibilityLabel — so a failing query is usually a real accessibility bug, not a test problem.
  • findBy* for anything async; never setTimeout, never a bare waitFor around a synchronous assertion.
  • One behaviour per test, named as a sentence.
import { screen, userEvent } from '@testing-library/react-native';
import { http, HttpResponse } from 'msw';
import { server } from '@/test/msw/server';
import { renderWithProviders } from '@/test/render';
import { OrdersScreen } from './OrdersScreen';

it('shows the empty state when there are no orders', async () => {
  server.use(http.get('*/orders', () => HttpResponse.json({ items: [] })));
  renderWithProviders(<OrdersScreen />);

  expect(await screen.findByText(/no orders yet/i)).toBeOnTheScreen();
});

it('opens an order when the row is pressed', async () => {
  const user = userEvent.setup();
  renderWithProviders(<OrdersScreen />);

  await user.press(await screen.findByRole('button', { name: /order 1001/i }));

  expect(await screen.findByText(/order details/i)).toBeOnTheScreen();
});

user.press() replays the full React Native touch sequence and takes at least ~130 ms of fake or real time, so prefer fake timers with userEvent.setup({ advanceTimers: jest.advanceTimersByTime }) in suites with many interactions.

4. Mobile cases worth a test

These are the ones that break in production and never in the simulator:

  • Offline: the request fails → cached content plus the offline banner, and a queued mutation shows as pending.
  • Permission denied: the Expo permission hook returns granted: false, canAskAgain: false → the "open settings" path renders.
  • Auth guard and deep links: signed out lands on sign-in; a link to a missing entity renders the not-found state instead of crashing (renderRouter with an initialUrl).
  • Platform forks and large fonts: .ios.tsx / .android.tsx pairs run on both platforms (jest-expo has per-platform presets), and a label at maxFontSizeMultiplier still fits its container.

5. What to mock, and what never to mock

  • Mock native Expo modules with no JS implementation (expo-secure-store, expo-camera, expo-notifications, expo-haptics), MMKV, and anything that reaches the device clock or the network stack directly.
  • Do not mock your own hooks, the query client, navigation, or the component under test's children. A test that mocks the thing it is testing proves only that the mock works.
  • Keep every module mock in src/test/mocks/ and register it once in the setup file, so a screen test never carries ten jest.mock calls at the top.

6. End-to-end with Maestro

Maestro drives the real binary through the accessibility tree with short YAML flows (launchApp with clearState: true, tapOn a visible label, assertVisible). Build the binary with an EAS profile using withoutCredentials, iOS simulator: true and Android buildType: apk, then run the flows from an EAS workflow on every pull request. Keep flows short, independent and seeded through the API — a flow depending on another flow's leftovers fails as soon as the suite is sharded. Flows, profile and workflow: references/navigation-and-e2e.md.

7. Deliverable format

## Behaviours to cover
1. <behaviour> — happy / error / empty / offline / permission-denied

## Tests
<file path>  (imports included, one it() per behaviour)

## Mocks and handlers
<Expo modules mocked, MSW handlers added or reused>

## How to run
npm test -- <path>   ·   maestro test .maestro/<flow>.yaml

Checklist

  • Behaviours listed before any test code was written
  • jest-expo preset configured, with the Reanimated resolver and setUpTests()
  • Queries are role/label/text based; every getByTestId justified in a comment
  • userEvent used for interaction; fake timers get advanceTimers
  • Network faked with MSW from msw/native, onUnhandledRequest: 'error'
  • Offline, error and permission-denied paths asserted, not just the happy path
  • Expo native modules mocked centrally; no mocks of the app's own hooks
  • No full-tree snapshots
  • Navigation-critical flows covered with renderRouter, not by asserting router calls
  • A small Maestro suite runs on a real build in CI, with seeded data

Anti-patterns

  • Snapshot tests of screens → they break on every style change and assert nothing a user cares about.
  • getByTestId everywhere → add roles and labels; the queries and the accessibility both improve.
  • fireEvent.press for a real user interactionawait user.press() fires the full sequence a device does.
  • Mocking useOrders to test OrdersScreen → fake the HTTP response instead and keep the hook real.
  • Asserting router.push was called → assert the destination screen rendered.
  • Importing setupServer from msw/nodemsw/native; the Node build needs http and fails to resolve.
  • A Detox and a Maestro suite in parallel → pick one; two flaky device suites get ignored twice as fast.
  • Testing that a component is memoised → the React Compiler makes it unobservable; assert output.

Go deeper

  • references/setup.md — full Jest config, jest.setup.ts, provider wrapper, Expo module and MMKV mocks, MSW for React Native, fake timers, per-platform runs.
  • references/navigation-and-e2e.mdrenderRouter recipes for guards, deep links and tabs; Maestro flows, EAS build profile and workflow, device-test CI shape.
  • Sibling skills: react-native-development (the code under test), react-native-architecture, react-native-performance, react-testing (the web equivalent), frontend-code-review.

References

Deeper material the agent loads only when needed.