Detox Setup & Architecture: How Gray-Box Testing Works in React Native
Detox is a gray-box end-to-end testing framework for React Native that synchronizes test execution with your app's internal state — eliminating the sleep() calls and timing hacks that make black-box tests brittle. This guide covers the architecture that makes Detox work and walks you through a complete setup for both iOS and Android.
Key Takeaways
- Gray-box = test runner knows app state — Detox hooks into the React Native bridge and waits for the app to be truly idle before interacting with elements.
- Two-process architecture — the test runner (Node.js) and the app (device/simulator) communicate over a WebSocket; understanding this explains most debugging scenarios.
- Platform-specific simulators matter — iOS uses xcrun simctl, Android uses AVD; Detox configuration must reference specific device types.
- Jest is the default test runner — Detox ships with a Jest adapter; Mocha is also supported but Jest is recommended.
- Environment setup is the hardest part — Xcode CLI tools, Android SDK, and proper PATH configuration catch most engineers off guard on first setup.
End-to-end tests for mobile apps have a reputation problem. They're slow, they fail randomly, and when they do fail it's usually because the test tapped a button before the app finished loading data. Detox was designed from the ground up to solve this problem with a fundamentally different architecture.
Why Black-Box Testing Fails for Mobile
Traditional mobile testing frameworks treat the app as an opaque black box. The test runner sends tap events, waits a fixed number of seconds, then checks what's on screen. This works until it doesn't — which is roughly 20% of the time in a typical CI run.
The fundamental problem is that "wait 2 seconds" is wrong. Sometimes the network is fast and 500ms is enough. Sometimes the device is under load and 2 seconds isn't enough. The test doesn't know which situation it's in because it has no insight into the app's state.
Detox solves this with gray-box testing.
The Gray-Box Architecture
Detox instruments the app at build time with a native synchronization module. This module monitors:
- Active network requests via the RN bridge
- JavaScript timers (setTimeout, setInterval)
- React Native's animation scheduler
- Native UI operations on the main thread
The test runner asks "is the app idle?" before every interaction. If the app is busy — waiting for a network response, animating a transition, processing a setState update — the test runner waits. When the app becomes idle, the test proceeds.
This eliminates an entire class of timing bugs without a single sleep() call.
The Two-Process Model
Detox runs two processes that communicate over a local WebSocket connection:
The tester process runs on your machine (or CI runner). It's a Node.js process running your Jest tests. When your test calls element(by.id('loginButton')).tap(), the tester process sends a message over the WebSocket to the second process.
The tested process is the React Native app running on a simulator or device. The Detox native module listens for messages from the tester, executes actions, waits for the app to become idle, then sends a response back.
Test Runner (Node.js)
|
WebSocket
|
Detox Native Module (embedded in your app)
|
React Native Bridge
|
Your App LogicThis architecture means Detox requires a special build of your app — the debug build, or a dedicated test build target that includes the Detox native module. You cannot run Detox against production builds.
Prerequisites
Before installing Detox, ensure you have:
- macOS for iOS testing (required — iOS simulators only run on macOS)
- Xcode 14+ with command-line tools installed:
xcode-select --install - Android Studio with at least one AVD configured
- Node.js 18+
- React Native CLI project (Expo is supported but requires bare workflow or special configuration)
Check your Xcode CLI tools installation:
xcode-select -p
# Should output: /Applications/Xcode.app/Contents/DeveloperInstalling Detox
1. Install the Detox CLI globally
npm install -g detox-cli2. Install Detox in your project
npm install detox --save-dev3. Install Jest adapter
npm install jest jest-circus --save-devConfiguring Detox
Detox configuration lives in your package.json under the detox key, or in a standalone detox.config.js file. The config file approach is cleaner for complex setups:
// detox.config.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',
},
},
devices: {
simulator: {
type: 'ios.simulator',
device: {
type: 'iPhone 15',
},
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_4_API_31',
},
},
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug',
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug',
},
},
};The most important concepts here:
apps define your app binaries and how to build them. The build command is run when you invoke detox build.
devices define the simulators and emulators available. For iOS, you reference a simulator type by name. For Android, you reference an AVD name.
configurations combine an app with a device to create a named configuration you run tests against.
Building the Test Binary
You cannot run Detox against a standard debug build from Metro. You need to build a dedicated test binary that includes the Detox native module:
# iOS
detox build --configuration ios.sim.debug
# Android
detox build --configuration android.emu.debugFor iOS, this runs the xcodebuild command defined in your config. For Android, it compiles both the main APK and the test APK (Detox requires both for Android instrumentation).
Important: Rebuild the binary whenever you change native code, add native dependencies, or upgrade Detox. Failing to rebuild is a common source of "Detox can't connect to app" errors.
Project Structure
Organize your e2e tests separately from unit tests:
your-app/
├── android/
├── ios/
├── src/ # App source code
├── __tests__/ # Jest unit tests
├── e2e/ # Detox tests
│ ├── jest.config.js
│ ├── setup.js
│ └── tests/
│ ├── login.test.js
│ ├── onboarding.test.js
│ └── checkout.test.js
└── detox.config.jsThe e2e Jest config:
// e2e/jest.config.js
module.exports = {
rootDir: '..',
testMatch: ['<rootDir>/e2e/tests/**/*.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,
};The maxWorkers: 1 setting is important — Detox tests run against a single simulator instance and cannot run in parallel by default (parallel execution requires separate simulators and is configured differently).
Your First Test
Once the build succeeds, write a basic smoke test:
// e2e/tests/smoke.test.js
describe('App', () => {
beforeAll(async () => {
await device.launchApp();
});
afterAll(async () => {
await device.terminateApp();
});
it('should show the home screen', async () => {
await expect(element(by.id('homeScreen'))).toBeVisible();
});
});Run it:
detox test --configuration ios.sim.debugDetox will:
- Launch the simulator
- Install the app binary
- Start the WebSocket server
- Run your tests
Understanding Synchronization
The synchronization mechanism is Detox's most important feature and the source of most configuration questions.
By default, Detox waits for the app to be idle after every action. But some apps have background timers or animations that never stop — Detox will hang waiting for idle state that never comes.
You can configure what Detox tracks:
// In your test setup or per-test
await device.setURLBlacklist(['.*analytics.*', '.*crashlytics.*']);Or temporarily disable synchronization for specific operations:
await device.disableSynchronization();
await element(by.id('infiniteScrollList')).scroll(500, 'down');
await device.enableSynchronization();Understanding when to disable synchronization — and forgetting to re-enable it — accounts for a significant portion of Detox test reliability issues.
iOS Simulator Configuration
For iOS, get the list of available simulators:
xcrun simctl list devices availableReference the exact name in your Detox config. Common names include iPhone 15, iPhone 15 Pro, iPad Air (5th generation).
If you see Error: No device found for type: iPhone 15, either the simulator isn't installed (add it via Xcode → Platforms) or the name doesn't exactly match what simctl reports.
Android Emulator Configuration
For Android, list available AVDs:
emulator -list-avdsIf the emulator doesn't start, common issues:
- HAXM not installed — install Intel HAXM from Android Studio SDK Manager
- KVM not available (Linux CI) — enable nested virtualization or use Google APIs system images
- AVD name has spaces — Detox handles this, but it's cleaner to use underscores
What's Next
With Detox installed and your first test running, the next steps are writing a comprehensive test suite that covers your app's critical flows, setting up proper test isolation with beforeEach/afterEach, and integrating with CI. Those topics deserve their own guides — the architecture here is the foundation everything else builds on.