Getting Started with Detox: E2E Testing for React Native Apps

Getting Started with Detox: E2E Testing for React Native Apps

End-to-end testing for mobile apps has historically been painful. Emulators crash, selectors break, and tests that pass locally fail in CI for mysterious reasons. Detox was built specifically to address these problems for React Native developers. Unlike generic automation tools, Detox is tightly integrated with the React Native runtime, giving it visibility that other frameworks simply don't have.

This guide walks you through installing Detox, configuring it for both iOS and Android, writing your first test, and running it in a real simulator.

What Is Detox?

Detox (Detached from Expectations) is a gray-box end-to-end testing framework for mobile apps. "Gray-box" means it doesn't just drive the UI from the outside — it also hooks into the app's internals to know when animations are running, network requests are in flight, or async operations are pending. This lets Detox wait intelligently instead of relying on sleep() calls.

Key characteristics:

  • JavaScript-first: tests written in Jest
  • No flaky sleeps: automatic synchronization with the app
  • Cross-platform: same test code for iOS and Android
  • CI-friendly: runs on GitHub Actions, Bitrise, CircleCI

Prerequisites

Before installing Detox, make sure you have:

  • Node.js 18+
  • React Native CLI project (Expo managed workflow has limited Detox support)
  • Xcode 14+ (for iOS)
  • Android Studio with a configured emulator (for Android)
  • applesimutils for iOS: brew tap wix/brew && brew install applesimutils

Installation

Install Detox as a dev dependency:

npm install detox --save-dev
# or
yarn add detox --dev

Install the Detox CLI globally:

npm install -g detox-cli

Project Configuration

Detox configuration lives in .detoxrc.js (or .detoxrc.json) at the project root.

// .detoxrc.js
/** @type {Detox.DetoxConfig} */
module.exports = {
  testRunner: {
    args: {
      '$0': 'jest',
      config: 'e2e/jest.config.js',
    },
    jest: {
      setupTimeout: 120000,
    },
  },
  apps: {
    'ios.debug': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/YourApp.app',
      build: 'xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
      reversePorts: [8081],
    },
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: { type: 'iPhone 14' },
    },
    emulator: {
      type: 'android.emulator',
      device: { avdName: 'Pixel_4_API_30' },
    },
  },
  configurations: {
    'ios.sim.debug': {
      device: 'simulator',
      app: 'ios.debug',
    },
    'android.emu.debug': {
      device: 'emulator',
      app: 'android.debug',
    },
  },
};

Create the e2e test directory:

mkdir e2e

Create the Jest config for Detox tests:

// e2e/jest.config.js
/** @type {import('@jest/types').Config.InitialOptions} */
module.exports = {
  rootDir: '..',
  testMatch: ['<rootDir>/e2e/**/*.test.js'],
  testTimeout: 120000,
  maxWorkers: 1,
  globalSetup: 'detox/runners/jest/globalSetup',
  globalTeardown: 'detox/runners/jest/globalTeardown',
  reporters: ['detox/runners/jest/reporter'],
  testEnvironment: 'detox/runners/jest/testEnvironment',
  verbose: true,
};

Writing Your First Test

Create e2e/firstTest.test.js:

describe('Example', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('should show welcome screen', async () => {
    await expect(element(by.id('welcomeText'))).toBeVisible();
  });

  it('should navigate to second screen on button press', async () => {
    await element(by.id('nextButton')).tap();
    await expect(element(by.id('secondScreenTitle'))).toBeVisible();
  });
});

To make elements selectable, add testID props to your React Native components:

// WelcomeScreen.js
export function WelcomeScreen({ onNext }) {
  return (
    <View>
      <Text testID="welcomeText">Welcome to MyApp</Text>
      <TouchableOpacity testID="nextButton" onPress={onNext}>
        <Text>Next</Text>
      </TouchableOpacity>
    </View>
  );
}

Building and Running

Build your app for the simulator first (this step is required before the first test run):

detox build --configuration ios.sim.debug

Run the tests:

detox test --configuration ios.sim.debug

For Android:

detox build --configuration android.emu.debug
detox test --configuration android.emu.debug

Common Element Selectors

Detox provides several matchers to locate elements:

// By testID (most reliable)
element(by.id('loginButton'))

// By text content
element(by.text('Submit'))

// By accessibility label
element(by.label('Close modal'))

// By type
element(by.type('RCTTextInput'))

// Combining matchers
element(by.id('listItem').and(by.text('Item 1')))

Common Actions

// Tap
await element(by.id('button')).tap();

// Long press
await element(by.id('item')).longPress();

// Type text
await element(by.id('emailInput')).typeText('user@example.com');

// Clear and type
await element(by.id('emailInput')).clearText();
await element(by.id('emailInput')).typeText('new@example.com');

// Scroll
await element(by.id('scrollView')).scroll(200, 'down');

// Swipe
await element(by.id('listView')).swipe('left', 'fast', 0.75);

Handling Navigation Between Screens

For apps using React Navigation, Detox handles screen transitions automatically because of its sync engine. You don't need to add artificial waits:

it('should complete login flow', async () => {
  await element(by.id('emailInput')).typeText('user@example.com');
  await element(by.id('passwordInput')).typeText('password123');
  await element(by.id('loginButton')).tap();
  
  // Detox waits for navigation to complete automatically
  await expect(element(by.id('dashboardTitle'))).toBeVisible();
});

Dealing with Permissions

Many apps need permissions (camera, location, notifications). Handle them in beforeAll:

beforeAll(async () => {
  await device.launchApp({
    permissions: {
      notifications: 'YES',
      location: 'always',
      camera: 'YES',
    },
  });
});

Debugging Failing Tests

When a test fails, Detox captures a screenshot and view hierarchy automatically. Check the artifacts directory after a test run.

To enable verbose logging:

detox test --configuration ios.sim.debug --loglevel verbose

To run only specific tests:

detox test --configuration ios.sim.debug --testNamePattern <span class="hljs-string">"should show welcome screen"

Structuring a Test Suite

For larger apps, organize tests by feature:

e2e/
  auth/
    login.test.js
    signup.test.js
    logout.test.js
  home/
    feed.test.js
    search.test.js
  profile/
    edit.test.js
    settings.test.js
  jest.config.js

What Detox Does and Doesn't Replace

Detox is powerful for verifying complete user journeys — login, checkout, form submission, navigation. It is not a replacement for unit tests on business logic or component tests on UI rendering. Think of the pyramid: many unit tests, fewer integration tests, and a focused set of E2E flows covering your most critical paths.

Integrating with HelpMeTest

If you want to monitor your React Native app's production behavior alongside your Detox E2E suite, HelpMeTest lets you run continuous tests against your live app environment — not just in CI, but on a schedule, with alerts when critical flows break. Detox covers the development cycle; HelpMeTest covers production monitoring.

Summary

Detox is the most mature E2E testing option for React Native. Its tight integration with the runtime eliminates the flakiness that plagues other mobile automation tools. Once you have the initial configuration in place — which is the hardest part — writing and maintaining tests becomes straightforward.

Start with your two or three most critical user journeys: login, the core feature, and checkout if applicable. Get those tests green and running in CI. Then expand coverage incrementally. The investment pays off every time you catch a regression before it reaches users.

Read more