How to Test Mobile Applications with Expo and React Native

Learn how to test mobile applications with Expo and React Native — from Jest unit tests to Detox E2E, CI, performance and accessibility.

Profile photo of RishavRishav
11th Sep 2026
Featured image for How to Test Mobile Applications with Expo and React Native

You're one merge away from release. The Expo build installs, the sign-in screen renders, and the happy path works on the simulator. Then a reviewer finds that a signed-in user can reach the onboarding route through a deep link, the API error leaves a spinner running forever, and the Android keyboard covers the submit button. None of those defects is exotic. They're the kind of failures that appear when a team treats mobile testing as a final pass instead of part of the app's design.

For an Expo and React Native app, the practical question isn't whether you can test everything. You can't. The question is how to test mobile applications by placing each risk at the cheapest layer that can detect it, then reserving device-level automation for behavior that depends on navigation, native capabilities, networking, or the operating system.

Table of Contents

Why Mobile Testing Matters Before You Ship

A build can install correctly and still fail before users reach it. Apple reviewed 7,771,599 submissions in 2024 and rejected 1,931,400, a rejection rate of about 24.9%, according to Apple App Store review statistics reported by Panto. The specific cause depends on the app, but the release risk is clear. Functional, policy, performance, and reliability defects can block distribution at the store gate.

Users give you less room to recover. 88% of users abandon an app after encountering bugs or glitches, while 51% abandon it completely after one or more bugs per day. A failed auth redirect, stale cache, or crash during payment is not just a QA ticket. It can turn into lost retention and support work.

Late defects also compete with feature delivery. The referenced data reports that fixing a bug after release can cost 30 times more than addressing it during design. That favors a front-loaded workflow over a release-week scramble. In an AppLighter Expo starter, test the supplied authentication, navigation, state, and API boundaries as soon as you customize them. Pre-wired code reduces setup work, but it does not remove the behavior those boundaries introduce.

An infographic illustrating why software testing is crucial for mobile application release, highlighting rejection risks, retention, and costs.An infographic illustrating why software testing is crucial for mobile application release, highlighting rejection risks, retention, and costs.

Reliability means more than a green simulator

A reliable Expo app behaves consistently across iOS, Android, and web, although runtime behavior, navigation, permissions, rendering, and network conditions differ. Expo limits platform-specific code, not platform-specific outcomes. Check resume behavior, offline requests, interrupted API calls, and deep links into protected routes.

Use the AppLighter boundaries to place each check:

  1. Unit tests cover pure logic, validation, transformations, and state transitions.
  2. Component tests verify loading, errors, disabled controls, and accessibility labels.
  3. Integration tests exercise navigation, providers, API adapters, and persistence together.
  4. End-to-end tests cover a small set of high-value journeys on a development build or device.
  5. Performance and accessibility checks catch failures that functional assertions miss.

A green simulator only proves one environment completed one run. Start with deterministic tests around the starter's existing Jest and API layers, then test navigation and native behavior at wider boundaries. Finish with a focused device matrix before distribution. Each check should answer a risk question. If it cannot, move it to a smaller test, a manual check, or remove it.

Building a Testing Strategy That Fits Expo Apps

The useful version of the test pyramid isn't a slogan about writing many unit tests. It's a placement tool. Put the most repeatable checks at the bottom, keep environment-dependent checks near the top, and make every E2E test earn its maintenance cost.

In an AppLighter-style Expo project, the boundaries are clear enough to guide that placement. Form rules and response mappers are unit-test material. A sign-in form with loading and error states belongs in component tests. A protected navigation transition needs integration coverage. A complete sign-in, session restore, and API-backed screen is a candidate for E2E.

An infographic showing the Expo Test Pyramid for mobile applications, illustrating unit, integration, and end-to-end testing.An infographic showing the Expo Test Pyramid for mobile applications, illustrating unit, integration, and end-to-end testing.

Assign tests by risk, not by screen count

A screen that rarely changes but controls account access deserves a different strategy from a frequently redesigned settings screen. Use two questions for every feature:

  • What happens if it fails? Authentication, payments, data loss, permissions, and deep links carry high consequence.
  • How often does it change? High-change code benefits from fast, local tests that give immediate feedback.

A useful decision matrix looks like this:

Feature ExampleRecommended LayerWhy This Layer
Password and email validationUnitPure rules run quickly and cover edge cases without rendering a device UI
Sign-in form statesComponentThe important contract is what the user sees and can interact with
Auth provider plus protected routeIntegrationThe risk sits between session state and navigation behavior
Sign-in through API to authenticated homeE2EThis verifies the complete user journey across app boundaries
API response normalizationUnit and integrationPure mapping is cheap to test, while the adapter confirms request behavior
Offline retry and resumeIntegration and E2ENetwork state and app lifecycle interact in ways isolated tests can't fully model
Typography and spacing adjustmentsComponent and visual reviewA full E2E test adds little signal for a presentational change

Keep the upper layers deliberately small

E2E tests are valuable, but they're slower to diagnose and more sensitive to build configuration, device state, animations, and backend data. Don't automate every validation rule through a simulator. Don't use a device test to prove that a selector returns an error for an empty string.

The practical split is many fast tests around logic and components, a balanced set around integrations, and a short list of business-critical journeys at the device level. That structure also makes failures easier to triage. A failed unit test points toward code. A failed integration test points toward a boundary. A failed E2E test may involve the app, build, device, API, test data, or automation itself.

Practical rule: If a test needs a real device to answer its question, keep it. If it only needs a device because the test is convenient to write that way, move it down the pyramid.

Unit and Component Testing With Jest and React Native Testing Library

AppLighter's pre-wired Jest setup changes the starting point. You don't need to assemble a runner before testing business logic, but you do need to keep the suite deterministic. The most valuable tests should run without a network, clock, native permission dialog, persisted session, or shared database state.

A typical Expo Jest configuration can stay close to the project's existing setup:

// jest.config.js
module.exports = {
  preset: 'jest-expo',
  setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
  testPathIgnorePatterns: ['/node_modules/', '/e2e/'],
  clearMocks: true,
  restoreMocks: true,
};

Use the project's installed Expo-compatible versions rather than copying a package matrix from an unrelated React Native app. In test/setup.ts, import the testing-library matchers and mock only the native behavior your test doesn't need to exercise.

// test/setup.ts
import '@testing-library/jest-native/extend-expect';

jest.mock('expo-secure-store', () => ({
  getItemAsync: jest.fn(),
  setItemAsync: jest.fn(),
  deleteItemAsync: jest.fn(),
}));

jest.mock('expo-linking', () => ({
  createURL: (path = '') => `app:///${path}`,
  parse: jest.fn(),
}));

Test behavior at the component boundary

React Native Testing Library should observe the UI the way a user does. Prefer accessible roles, labels, and text over internal component names or implementation-specific state. A focused sign-in test might look like this:

import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { SignInForm } from '@/features/auth/SignInForm';
import { signIn } from '@/lib/api/auth';

jest.mock('@/lib/api/auth', () => ({
  signIn: jest.fn(),
}));

test('shows an API error after a failed sign-in', async () => {
  (signIn as jest.Mock).mockRejectedValueOnce(new Error('Invalid credentials'));

  render(<SignInForm />);

  fireEvent.changeText(screen.getByLabelText('Email'), 'person@example.com');
  fireEvent.changeText(screen.getByLabelText('Password'), 'wrong-password');
  fireEvent.press(screen.getByRole('button', { name: 'Sign in' }));

  expect(await screen.findByText('Invalid credentials')).toBeTruthy();
  expect(screen.getByRole('button', { name: 'Sign in' })).toBeEnabled();
});

This test doesn't care whether the form uses local state, a store, or a particular component hierarchy. It protects the contract that matters: the user submits credentials, the request fails, and the interface recovers visibly.

For AppLighter features, I usually separate tests into four small groups:

  • Pure logic: validation, query construction, response mapping, feature flags, and date formatting.
  • State transitions: loading, success, failure, session restoration, sign-out, and reset behavior.
  • Navigation guards: authenticated users shouldn't see onboarding, while signed-out users shouldn't reach protected screens.
  • Accessible interaction: labels, roles, focus order, disabled states, keyboard behavior, and error announcements.

The API layer deserves its own seam. Mock the Hono client or Supabase adapter at the boundary, then test the adapter's request shape separately. Don't let every component test instantiate a real client. That creates slow, shared-state tests that fail for reasons unrelated to the component.

A developer coding React Native tests on a laptop with a terminal showing successful test results.A developer coding React Native tests on a laptop with a terminal showing successful test results.

Make test state hermetic

Reset stores between tests. Provide a fresh query client or provider tree for each render. Mock time when a component depends on token expiry or retry intervals. If you use Zustand, expose a test reset helper or create the store inside the test harness instead of allowing state to leak between cases.

A useful reference for keeping this layer focused is AppLighter's unit testing best practices. The principle is straightforward: assert user-visible outcomes and stable contracts, not private implementation details that will change during normal development.

Integration and End to End Testing With Detox and Alternatives

A complete mobile flow crosses boundaries that unit tests intentionally avoid. The app loads configuration, mounts providers, restores a session, calls the API, updates state, changes navigation, and renders a screen. Integration and E2E tests tell you whether those pieces still agree.

For Expo, the first choice is build type. Expo Go is useful for rapid development, but native-module behavior and production configuration aren't identical to a development build. Detox needs an installable app binary and native build integration, so use an Expo development build or an EAS-generated build when the scenario depends on native behavior.

Detox, Playwright, and Appium have different jobs

Detox is a strong fit for React Native in-app flows. Its synchronization model can reduce timing problems because it understands more about the app under test, but the setup is more involved than Jest and can become sensitive to native build changes. It's a good choice for a compact set of stable journeys such as sign-in, tab navigation, and sign-out.

Playwright belongs primarily with the web surface of an Expo project. It can validate responsive layouts and browser behavior for the web build, but it doesn't test native app screens. Treat web parity as a separate target rather than pretending browser emulation proves iOS or Android behavior.

Appium makes more sense when you need system-level interaction, broad native compatibility, WebViews, permissions, notifications, or cross-app behavior. The flexibility comes with more orchestration and more timing work. If your AppLighter app stays within React Native screens and needs fast developer feedback, Appium can be unnecessary overhead.

A comparison chart showing setup complexity, speed, and platform focus for Detox, Playwright, and Appium testing tools.A comparison chart showing setup complexity, speed, and platform focus for Detox, Playwright, and Appium testing tools.

A minimal Detox configuration might look like this, with actual simulator and build commands adjusted to your native project:

{
  "detox": {
    "testRunner": {
      "args": {
        "config": "e2e/jest.config.js"
      }
    },
    "apps": {
      "ios.debug": {
        "type": "ios.app",
        "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/App.app",
        "build": "xcodebuild -workspace ios/App.xcworkspace -scheme App -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build"
      }
    },
    "devices": {
      "simulator": {
        "type": "ios.simulator",
        "device": {
          "type": "iPhone"
        }
      }
    },
    "configurations": {
      "ios.sim.debug": {
        "device": "simulator",
        "app": "ios.debug"
      }
    }
  }
}

Use stable selectors in the app:

<Pressable
  accessibilityRole="button"
  accessibilityLabel="Sign in"
  testID="auth.sign-in"
  onPress={submit}
>
  <Text>Sign in</Text>
</Pressable>

Then keep the E2E flow explicit and state-aware:

await element(by.id('auth.email')).typeText('e2e-user@example.com');
await element(by.id('auth.password')).typeText('valid-test-password');
await element(by.id('auth.sign-in')).tap();
await expect(element(by.id('home.screen'))).toBeVisible();

Don't add arbitrary sleeps. Wait for a visible state, use framework synchronization, and make the backend data disposable or resettable. A sign-in test that depends on yesterday's user record will eventually fail for a reason the test can't explain.

The AppLighter guide to end-to-end testing is useful when you're choosing between Detox and lighter flow-oriented tools. The selection should follow the boundary you need to cross, not the popularity of the framework.

Running Tests in CI and on Real Devices Plus Performance and Accessibility

A local green run is evidence, not a release process. CI should install dependencies from a lockfile, run static checks, execute Jest in parallel where practical, build the app with the intended Expo configuration, and preserve logs and screenshots when a device test fails.

A GitHub Actions workflow can stay intentionally boring:

name: mobile-checks

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'yarn'
      - run: yarn install --immutable
      - run: yarn lint
      - run: yarn typecheck
      - run: yarn test --ci --coverage

Build and device jobs can run separately because native builds have different requirements from JavaScript tests. Use EAS profiles for predictable configuration, keep secrets outside test logs, and upload failure artifacts. AppLighter's CI/CD guidance covers the broader release pipeline, including validation and internal distribution.

Choose a representative device matrix

Universal coverage isn't realistic. Thousands of Android models, multiple iOS versions, foldables, and cross-platform differences create a matrix too large for manual QA, and recent industry coverage identifies automation scaling cost and in-house device testing burden as major organizational challenges in mobile testing trends.

Build the matrix from install-base analytics. Cover the devices that represent about 80% of your users, then add at least one low-end handset, one tablet, and one vendor-specific Android skin, as recommended in TestMu AI's mobile testing guidance. Review that matrix quarterly because your real audience changes.

Test the failures that simulators hide

Performance testing should observe startup, screen transition responsiveness, memory behavior, image loading, list rendering, and network retries. Don't reduce performance to a single benchmark. A release candidate should behave acceptably on a constrained device and a poor connection, not only on the developer laptop.

Accessibility needs automated assertions plus manual interaction. Check accessible names, roles, focus order, dynamic text, contrast, touch targets, and screen-reader announcements. Then validate critical flows with VoiceOver and TalkBack. A component can pass a role assertion and still produce an unusable focus sequence.

Mobile reliability defects often appear outside the happy path. Crash-related issues account for roughly 70% to 71% of uninstalls, while slow loading drives close to 70% of users to abandon an app, according to mobile app testing data from ElectroIQ. Add checks for background and foreground transitions, interrupted requests, offline recovery, foldable state changes, multi-window layouts, 5G behavior, wearables where relevant, and on-device AI behavior. These conditions are difficult to reproduce, but they're part of the environments real users create.

Practical Tips Debugging and Your Release Ready Checklist

A flaky test is not harmless noise. Once developers stop trusting failures, the suite becomes decoration. Track unstable tests, capture device logs and screenshots, record the failed selector and app state, and quarantine a test only with an owner and a repair task.

Start debugging from the boundary that failed. A Jest failure usually belongs to logic, mocks, or state isolation. A component failure may expose an accessibility or async-rendering issue. A Detox failure needs inspection of the build, device logs, synchronization, selector, and backend state before you add a retry.

Hermetic data matters most in auth and API flows. Create a known test user or stub the API, reset persisted storage between runs, and avoid tests that depend on execution order. For network failures, assert the recovery path rather than waiting for a request to eventually succeed.

Use this release checklist for an AppLighter Expo project:

  • Unit coverage: Validation, state transitions, response mapping, and error handling run without network access.
  • Component behavior: Loading, empty, error, disabled, keyboard, and accessibility states are asserted through visible behavior.
  • Navigation integration: Protected routes, deep links, session restoration, sign-out, and back behavior work together.
  • API integration: Hono and Supabase boundaries handle success, authorization failure, timeout, malformed data, and retry states.
  • E2E journeys: A small set of critical flows passes on the intended development or release build.
  • Device coverage: The install-base matrix includes low-end, tablet, and vendor-specific behavior, with the matrix reviewed quarterly.
  • Performance: Startup, memory, lists, images, slow networks, background resume, and interrupted requests have been checked.
  • Accessibility: Automated semantics and manual VoiceOver or TalkBack passes cover the important journeys.
  • Store readiness: Permissions, deep links, configuration, privacy behavior, and production API settings are verified before submission.

Testing works when it remains part of everyday development. Keep fast feedback close to the code, keep device tests narrow and meaningful, and fix flaky infrastructure before asking the team to trust another release signal.


AppLighter provides an Expo and React Native starter with authentication, navigation, state management, a Supabase-backed data layer, and a Hono TypeScript API layer already wired into the project shape. Use AppLighter to start with those boundaries in place, then apply this layered testing workflow before your next iOS, Android, or web release.

Stay Updated on the Latest UI Templates and Features

Be the first to know about new React Native UI templates and kits, features, special promotions and exclusive offers by joining our newsletter.