Introduction
Detox is an end-to-end testing framework for React Native applications that enables gray-box testing on real devices and simulators. It provides reliable, fast, and deterministic testing by synchronizing with the React Native bridge and native layers. Detox eliminates flakiness common in mobile testing by automatically waiting for the app to be idle before executing test actions.
Why Detox Matters:
- Reduces manual testing effort and human error
- Provides consistent test results across different environments
- Enables continuous integration for mobile apps
- Catches integration issues early in development
- Supports both iOS and Android platforms
Core Concepts & Principles
Test Structure
- Describe blocks: Group related tests
- Before/After hooks: Setup and teardown operations
- Test cases: Individual test scenarios
- Matchers: Assertions to verify expected behavior
- Actions: User interactions (tap, type, scroll)
Synchronization Model
- Automatic synchronization: Waits for app to be idle
- Network idle: Waits for network requests to complete
- Animation idle: Waits for animations to finish
- Timer idle: Waits for JavaScript timers
- React Native bridge idle: Waits for bridge communication
Device Management
- Device allocation: Manages simulator/device instances
- App installation: Handles app deployment
- App lifecycle: Controls app launch, termination, and reloading
Installation & Setup Process
Step 1: Install Detox CLI
npm install -g detox-cli
Step 2: Add Detox to Project
npm install detox --save-dev
Step 3: Initialize Detox Configuration
detox init -r jest
Step 4: Configure detox.config.js
module.exports = {
testRunner: 'jest',
runnerConfig: 'e2e/config.json',
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 12'
}
},
emulator: {
type: 'android.emulator',
device: {
avdName: 'Pixel_3_API_29'
}
}
},
configurations: {
'ios.sim.debug': {
device: 'simulator',
app: 'ios.debug'
},
'android.emu.debug': {
device: 'emulator',
app: 'android.debug'
}
}
};
Step 5: Build and Test
detox build --configuration ios.sim.debug
detox test --configuration ios.sim.debug
Essential Testing Techniques
Element Selection Methods
| Method | Usage | Example |
|---|---|---|
by.id() | Select by testID | element(by.id('loginButton')) |
by.text() | Select by visible text | element(by.text('Login')) |
by.label() | Select by accessibility label | element(by.label('User Profile')) |
by.type() | Select by component type | element(by.type('RCTScrollView')) |
by.traits() | Select by accessibility traits | element(by.traits(['button'])) |
Action Methods
| Action | Purpose | Syntax |
|---|---|---|
tap() | Tap element | await element(by.id('button')).tap() |
longPress() | Long press element | await element(by.id('item')).longPress() |
typeText() | Type text into input | await element(by.id('input')).typeText('hello') |
replaceText() | Replace all text | await element(by.id('input')).replaceText('new text') |
clearText() | Clear text field | await element(by.id('input')).clearText() |
scroll() | Scroll element | await element(by.id('scrollView')).scroll(200, 'down') |
scrollTo() | Scroll to edge | await element(by.id('scrollView')).scrollTo('bottom') |
swipe() | Swipe gesture | await element(by.id('card')).swipe('left') |
Assertion Methods
| Matcher | Purpose | Example |
|---|---|---|
toBeVisible() | Element is visible | await expect(element(by.id('title'))).toBeVisible() |
toExist() | Element exists in hierarchy | await expect(element(by.id('hidden'))).toExist() |
toHaveText() | Element has specific text | await expect(element(by.id('label'))).toHaveText('Success') |
toHaveLabel() | Element has accessibility label | await expect(element(by.id('button'))).toHaveLabel('Submit') |
toHaveId() | Element has specific testID | await expect(element(by.text('Login'))).toHaveId('loginBtn') |
toHaveValue() | Input has specific value | await expect(element(by.id('input'))).toHaveValue('test@example.com') |
Advanced Testing Patterns
Waiting Strategies
// Wait for element to be visible
await waitFor(element(by.id('loading'))).toBeNotVisible().withTimeout(5000);
// Wait for element to exist
await waitFor(element(by.id('newItem'))).toExist().withTimeout(3000);
// Wait while element is visible
await waitFor(element(by.id('spinner'))).toBeNotVisible();
Device Interactions
// Device orientation
await device.setOrientation('landscape');
await device.setOrientation('portrait');
// Device permissions
await device.requestPermissions('camera');
await device.requestPermissions('location');
// App state management
await device.launchApp();
await device.terminateApp();
await device.reloadReactNative();
// Background/foreground
await device.sendToHome();
await device.launchApp();
Multi-element Operations
// Select multiple elements
await element(by.id('list')).atIndex(0).tap();
// Work with collections
const items = element(by.id('itemList'));
await expect(items).toHaveLength(5);
Configuration Options
Test Runner Configuration (Jest)
// e2e/config.json
{
"setupFilesAfterEnv": ["<rootDir>/init.js"],
"testEnvironment": "node",
"testRunner": "jest-circus/runner",
"testTimeout": 120000,
"testRegex": "\\.e2e\\.js$",
"reporters": ["detox/runners/jest/streamlineReporter"],
"verbose": true
}
Environment Variables
| Variable | Purpose | Example |
|---|---|---|
DETOX_CONFIGURATION | Override config | ios.sim.release |
DETOX_LOGLEVEL | Set log level | verbose, debug, info |
DETOX_CLEANUP | Cleanup after tests | true, false |
DETOX_DEVICE_BOOT_ARGS | Device boot arguments | --gpu swiftshader_indirect |
Common Challenges & Solutions
Challenge: Flaky Tests
Solutions:
- Use proper synchronization with
waitFor() - Avoid hardcoded delays with
sleep() - Ensure elements are stable before interaction
- Use reliable selectors (testID over text)
Challenge: Element Not Found
Solutions:
- Verify element hierarchy with device logs
- Check if element is rendered conditionally
- Use
waitFor()for dynamic content - Ensure proper testID assignment
Challenge: Slow Test Execution
Solutions:
- Use
device.reloadReactNative()instead of full app restart - Optimize app build configuration
- Run tests in parallel when possible
- Use appropriate timeout values
Challenge: Platform-Specific Issues
Solutions:
- Create platform-specific test files
- Use conditional logic for platform differences
- Maintain separate device configurations
- Test on multiple OS versions
Best Practices & Tips
Test Organization
- Group related tests in describe blocks
- Use descriptive test names that explain the scenario
- Implement proper setup and teardown in hooks
- Maintain independent test cases that don’t rely on each other
Element Selection
- Always use
testIDfor reliable element identification - Avoid selecting elements by text that might change
- Create a page object model for complex screens
- Use accessibility labels as fallback selectors
Test Data Management
- Use test-specific data that won’t conflict
- Reset app state between test suites
- Mock external dependencies when possible
- Use factories for creating test data
Performance Optimization
- Build once, test multiple scenarios
- Use
device.reloadReactNative()for faster resets - Implement parallel test execution
- Monitor test execution times and optimize slow tests
Debugging Strategies
- Enable verbose logging during development
- Take screenshots on test failures
- Use device logs to troubleshoot issues
- Implement custom matchers for complex assertions
Sample Test Structure
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
beforeEach(async () => {
await device.reloadReactNative();
});
afterAll(async () => {
await device.terminateApp();
});
it('should login with valid credentials', async () => {
// Navigate to login screen
await element(by.id('loginTab')).tap();
// Enter credentials
await element(by.id('emailInput')).typeText('test@example.com');
await element(by.id('passwordInput')).typeText('password123');
// Submit form
await element(by.id('loginButton')).tap();
// Verify success
await waitFor(element(by.id('homeScreen')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.id('welcomeMessage')))
.toHaveText('Welcome back!');
});
it('should show error for invalid credentials', async () => {
await element(by.id('loginTab')).tap();
await element(by.id('emailInput')).typeText('invalid@example.com');
await element(by.id('passwordInput')).typeText('wrongpassword');
await element(by.id('loginButton')).tap();
await expect(element(by.id('errorMessage')))
.toBeVisible();
});
});
CI/CD Integration
GitHub Actions Example
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm install
- name: Build iOS app
run: detox build --configuration ios.sim.debug
- name: Run iOS tests
run: detox test --configuration ios.sim.debug
Debugging Commands
| Command | Purpose |
|---|---|
detox test --loglevel verbose | Detailed logging |
detox test --take-screenshots all | Screenshots on actions |
detox test --record-logs all | Record device logs |
detox test --headless false | Show simulator |
detox test --debug-synchronization | Debug sync issues |
Resources for Further Learning
Official Documentation
Community Resources
Video Tutorials
- Wix Engineering Blog posts on Detox
- React Native EU conference talks
- YouTube tutorials on mobile E2E testing
Best Practice Guides
Quick Reference Commands:
# Initialize project
detox init -r jest
# Build app
detox build --configuration <config-name>
# Run tests
detox test --configuration <config-name>
# Clean build
detox clean-framework-cache && detox build-framework-cache
# Run specific test file
detox test e2e/login.e2e.js --configuration ios.sim.debug
