Skip to main content
Back to module
Mobile App Production · Quality

Testing mobile apps

Reading1h 2mLesson 1 of 3 in this module

Write unit and component tests with Jest and React Native Testing Library, and an e2e flow.

Why it matters

Mobile releases are slow to undo. A web bug can be fixed in minutes; a broken mobile build may sit in users' hands until store review approves the fix and every user updates. Automated tests catch regressions before that happens and let a team change code with confidence. Mid-level mobile developers are expected to write them as a normal part of every feature.

The testing pyramid for mobile

  • Unit tests (many, fast): plain functions and reducers. Price rules, validation, the navigation and cache logic you wrote earlier. Run with Jest.
  • Component tests (a good number): render one component or screen in a simulated environment and interact with it like a user. Use React Native Testing Library (RNTL) with Jest.
  • End-to-end (e2e) tests (a few, slow): drive the real app on a simulator, emulator or device, for the journeys that must never break (sign in, checkout). Use Maestro (flows written in YAML) or Detox (tests written in JavaScript, synchronised with the app).
  • Manual testing on real devices for what automation misses: feel, gestures, small screens, slow phones.

Setting up Jest in Expo

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

Add to package.json:

{
  "scripts": { "test": "jest --watchAll=false" },
  "jest": { "preset": "jest-expo" }
}

The jest-expo preset mocks native modules so components render in Node without a phone.

Worked example: a component test

The component:

export function Counter({ initial = 0 }) {
  const [count, setCount] = useState(initial);
  return (
    <View>
      <Text>Count: {count}</Text>
      <Pressable accessibilityRole="button" onPress={() => setCount(count + 1)}>
        <Text>Add one</Text>
      </Pressable>
    </View>
  );
}

The test, written the way a user would interact:

import { render, screen, fireEvent } from "@testing-library/react-native";
import { Counter } from "./Counter";

test("adds one when the button is pressed", () => {
  render(<Counter initial={2} />);
  fireEvent.press(screen.getByRole("button", { name: "Add one" }));
  expect(screen.getByText("Count: 3")).toBeOnTheScreen();
});

The query getByRole("button", { name: ... }) finds the element by its accessibility role and label, so the test also checks that the button is accessible. Prefer role and text queries over test ids; reach for testID only when nothing user-visible identifies an element. Recent versions of RNTL include the toBeOnTheScreen matcher; older setups need @testing-library/jest-native.

For screens that fetch data, inject a fake API (from the architecture lesson) or mock the module, and use findByText (which waits) for content that appears after loading.

Worked example: an e2e flow with Maestro

Maestro drives the app through the accessibility tree. A flow is a YAML file:

appId: com.yourname.studytimer
---
- launchApp
- tapOn: "Add one"
- assertVisible: "Count: 1"

Run it with maestro test flows/counter.yaml against a running Android emulator or iOS Simulator (Maestro's CLI runs on macOS, Linux and Windows; the iOS Simulator needs a Mac). Detox does the same job with JavaScript tests and waits automatically for animations and network calls to settle, at the cost of a more involved setup. EAS can run e2e tests on its build servers as part of a build.

What to test

  • Every reducer and pure function: unit tests.
  • Every screen's main states: loading, data, empty, error.
  • Interactions that change state: taps, typing, toggles.
  • One or two e2e journeys per critical flow.
  • Bugs: write a failing test that reproduces each fixed bug, so it stays fixed.

Common mistakes

  • Testing implementation details (state variable names, internal functions) so tests break on every refactor. Test what the user sees.
  • Snapshot tests of whole screens that everyone updates without reading.
  • Real network calls in unit or component tests: slow and flaky. Mock or inject.
  • Too many e2e tests: they are slow and brittle. Keep them for critical paths.
  • Timing-based waits (sleep(2000)). Use findBy... queries or the e2e tool's built-in waiting.

Quick recap

  • Many unit tests, a solid layer of component tests with React Native Testing Library, a few e2e flows with Maestro or Detox.
  • Query by role and text, the way a user finds things.
  • Mock the network, cover loading/empty/error states and pin every fixed bug with a test.

Try it

The Do tab has you add Jest and React Native Testing Library to your project, write a component test, and optionally run a Maestro flow on an emulator.

Also in this lesson: