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)
applesimutilsfor iOS:brew tap wix/brew && brew install applesimutils
Installation
Install Detox as a dev dependency:
npm install detox --save-dev
# or
yarn add detox --devInstall the Detox CLI globally:
npm install -g detox-cliProject 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 e2eCreate 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.debugRun the tests:
detox test --configuration ios.sim.debugFor Android:
detox build --configuration android.emu.debug
detox test --configuration android.emu.debugCommon 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 verboseTo 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.jsWhat 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.