Developing E2E Tests for Mobile Applications (Detox)
Detox is gray-box testing. Unlike Appium, which works outside the application like a black box, Detox embeds a test server directly into the application process. This gives it one critical advantage: synchronization with React Native event loop. Detox knows when JS thread is busy, when animations run, when network requests execute — and waits automatically. Hence significantly fewer sleep() calls and unstable tests compared to Appium on React Native.
Configuration: First Pitfalls
Detox setup takes more time than expected. Especially on iOS.
package.json (Detox 20.x):
{
"detox": {
"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/MyApp.app",
"build": "xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -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", "os": "iOS 17.4" }
},
"emulator": {
"type": "android.emulator",
"device": { "avdName": "Pixel_7_API_34" }
}
}
}
}
Common problem on first setup: binary built with wrong flags. For Detox on Android APK must be built with assembleAndroidTest — without this synchronization doesn't work. On iOS — simulator build only (-sdk iphonesimulator), real devices require separate profiling and signing.
Writing Tests
Detox uses matchers similar to Espresso/XCTest, but with unified API:
describe('Auth flow', () => {
beforeAll(async () => {
await device.launchApp({ newInstance: true });
});
beforeEach(async () => {
await device.reloadReactNative(); // fast reset without restart
});
it('should login successfully', async () => {
await element(by.id('email_input')).typeText('[email protected]');
await element(by.id('password_input')).typeText('password123');
await element(by.id('login_button')).tap();
await expect(element(by.id('home_screen'))).toBeVisible();
});
});
by.id() searches by testID (React Native prop). This is main way — not XPath, not text. Convention: all interactive elements in components get testID with naming convention (screen_component_action).
device.reloadReactNative() faster than device.launchApp({ newInstance: true }). Use reloadReactNative() between tests in one describe block, newInstance: true — when clean native state needed (e.g., after push notification tests).
Working with Animations and Asynchronicity
Detox automatically waits for completion of:
- JS operations (Promise, setTimeout, setInterval)
- Requests via
fetchandXMLHttpRequest(if URL not in whitelist) - Native animations via React Native Animated
But infinite animations (loop: true) block waiting. Solution — disable animations in test build through flag:
await device.launchApp({
launchArgs: { detoxDisableHierarchyDump: 'YES' },
permissions: { notifications: 'YES', camera: 'YES' },
});
Or disable through React Native: in __DEV__ mode or through custom flag IS_TESTING pass AnimationTesting.setEnabled(false).
Parallel Testing
Detox supports parallel execution through jest sharding:
detox test --configuration android.debug --workers 3
With --workers 3 Detox launches 3 emulators in parallel and distributes test files between them. AVDs must be created beforehand (Pixel_7_API_34_1, Pixel_7_API_34_2, Pixel_7_API_34_3) or Detox creates them if permissions allow.
Same on iOS — 3 simulators simultaneously. Requires macOS machine with enough RAM (16 GB minimum for 3 simulators).
CI Integration
GitHub Actions with macOS runner for iOS:
jobs:
e2e-ios:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci
- run: npx pod-install
- run: npx detox build --configuration ios.debug
- run: npx detox test --configuration ios.debug --headless
For Android — ubuntu-latest with reactivecircus/android-emulator-runner@v2:
- uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
profile: pixel_7
script: npx detox test --configuration android.debug --headless
--headless — mandatory in CI. Without it on Ubuntu emulator looks for display and crashes.
Timeline
5 days — Detox configuration + coverage of main user flows (authorization, navigation, key screens). With complex native modules (Push, Biometrics, Camera) add 1–2 days. Cost is calculated individually.







