What to Test in an App

A sensible suite: fast logic tests, a layer of component tests, a few real device journeys, and jest-expo running green.

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

A test suite is code that runs your code and fails the build when the answer is wrong. On mobile the payoff is larger than on a server: a bug you ship has to pass a store review before the fix reaches anyone.

Three layers, very different costs

Logic tests cover plain functions, reducers and hooks. They run in Node in milliseconds, so you can have hundreds. Component tests render one component and assert what a user would see; fast enough for dozens. End-to-end tests drive the real app on a simulator, and each one takes minutes, so you automate three journeys, not thirty.

None of this opens a browser. There is no DOM here, no document and no querySelector. A component test renders a tree of native components, which you query by text and by accessibility role.

Getting the first run green

Expo SDK 57 pairs with jest-expo 57, a Jest preset that mocks the native half of the Expo SDK so the imports resolve under Node. Add @testing-library/react-native 14 for component tests; it replaces react-test-renderer, which is deprecated under React 19.

What trips people up

Two things. The preset mocks Expo's own modules only, so other native libraries stay unmocked until you write the mock yourself. And version 14 of the testing library made render, fireEvent and act asynchronous, so an older example copied from a blog post fails on a missing await.

Example

Example · tsx
// __tests__/Greeting.test.tsx
import { render, screen } from '@testing-library/react-native';
import { greetingFor } from '../lib/greeting';
import Greeting from '../components/Greeting';
// Greeting renders: <Text>{`${greetingFor(hour)}, ${name}`}</Text>

describe('greetingFor', () => {
  // Layer one: a pure function. No React, no device, microseconds to run.
  it('splits the day into three parts', () => {
    expect(greetingFor(8)).toBe('Good morning');
    expect(greetingFor(13)).toBe('Good afternoon');
    expect(greetingFor(21)).toBe('Good evening');
  });

  it('treats midday as the afternoon boundary', () => {
    expect(greetingFor(12)).toBe('Good afternoon');
  });
});

describe('<Greeting />', () => {
  // Layer two: the component. hour is a prop, so the test owns the clock.
  it('shows the greeting and the name together', async () => {
    // render() is async in React Native Testing Library 14: await it.
    await render(<Greeting name="Ada" hour={8} />);

    expect(screen.getByText('Good morning, Ada')).toBeOnTheScreen();
  });
});

When to use it

  • Decide which parts of a new feature deserve tests before writing any of them.
  • Add Jest and a first passing test to an Expo app that has never had one.
  • Replace brittle full-tree snapshots with assertions that state what the screen shows.
  • Explain why one test file reports four times in a jest-expo/universal run.

More examples

Jest configuration in package.json

The short form is "jest": { "preset": "jest-expo" }, and it is the usual starting point, but for legacy reasons it runs your tests in a single native (iOS) environment. The multi-platform preset is "jest-expo/universal", which runs every test file once per platform target including web and node, so one broken assertion is reported four times. Listing the projects explicitly keeps the run to the two platforms the app ships on. The preset also sets transformIgnorePatterns for you; extend that only when a package in node_modules ships untranspiled source.

Example · json
{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:ci": "jest --ci --coverage"
  },
  "jest": {
    "projects": [
      { "preset": "jest-expo/ios" },
      { "preset": "jest-expo/android" }
    ]
  }
}

Installing and running the suite

Reach for this when adding tests to an existing Expo app. Using expo install rather than npm install is what keeps jest-expo on the same major version as the SDK: SDK 57 wants jest-expo 57. Testing library 14 requires React 19 and Node 22.13 or newer, which an Expo SDK 57 project already satisfies.

Example · bash
# expo install picks versions that match the installed SDK.
npx expo install jest-expo jest @types/jest --dev

# Testing library 14 needs test-renderer 1.x as a peer dependency.
npx expo install @testing-library/react-native test-renderer --dev

npm test                       # whole suite, once
npx jest Greeting              # only files matching "Greeting"
npx jest -t 'splits the day'   # only tests whose name matches

Assertions instead of a whole-tree snapshot

A snapshot test would catch both of these changes too, but it fails on any reshuffle of the tree, and nobody reading a failed snapshot can tell whether the change was intended. Explicit assertions say what the component promises. Keep snapshots for small, stable output, and never let a reviewer approve one they have not read.

Example · tsx
// __tests__/PriceTag.test.tsx
import { render, screen } from '@testing-library/react-native';
import PriceTag from '../components/PriceTag';

it('formats the price and marks it as reduced', async () => {
  await render(<PriceTag cents={2599} onSale />);

  // Each line names one fact a user can see.
  expect(screen.getByText('$25.99')).toBeOnTheScreen();
  expect(screen.getByText('Sale')).toBeOnTheScreen();
});

it('hides the badge when the item is not reduced', async () => {
  await render(<PriceTag cents={2599} onSale={false} />);

  // queryBy* returns null instead of throwing, so it can prove absence.
  expect(screen.queryByText('Sale')).not.toBeOnTheScreen();
});

Discussion

  • Be the first to comment on this lesson.