Skip to main content
Back to blog

Mobile Automation: From 40% Flaky Tests to 95% Stability

A deep dive into building reliable mobile test automation with Detox and WebDriverIO. Learn the patterns that transformed our flaky test suite into a stable CI/CD pipeline.

9 min read
by Andrii Peretiatko
Mobile TestingDetoxWebDriverIOReact NativeAppiumCI/CD

The Mobile Testing Nightmare

Mobile test automation has a reputation problem. Ask any QA engineer about their mobile tests, and you'll likely hear:

  • "They work on my machine but fail in CI"
  • "Tests are too slow - 2 hours for a full run"
  • "We spend more time fixing tests than finding bugs"
  • "Flakiness is just part of mobile testing"

I'm here to tell you it doesn't have to be this way.

At EPAM Systems, our React Native app had zero mobile automation. Manual testing took 3 days per release. When we finally introduced automated tests with Appium, we quickly hit 40% flakiness—tests randomly failing for no clear reason.

After 6 months of iteration, we achieved 95% test stability and reduced execution time from 2+ hours to 18 minutes. Here's how.


Why Mobile Tests Are Flaky (And How to Fix It)

Root Cause #1: Timing Issues

Mobile apps are asynchronous by nature. Network calls, animations, and state updates happen unpredictably. Traditional sleep-based waits cause both flakiness and slowness:

The problem: If the network is slow, 3 seconds isn't enough. If it's fast, you're wasting 2.9 seconds.

The solution: Smart synchronization

Detox has built-in synchronization with React Native's event loop, making this seamless:


Root Cause #2: Element Identification

Finding elements reliably across iOS and Android is challenging. XPath selectors break easily, accessibility labels aren't always set, and resource IDs differ by platform.

The wrong way: Fragile locators

The right way: Test IDs

Best practice: Establish a naming convention for testID:

[screen]-[component]-[action?]

Examples:
- login-email-input
- login-password-input
- login-submit-button
- dashboard-logout-button

Root Cause #3: Test Data Pollution

Mobile tests often share state—databases, user sessions, API responses. When one test fails, it can cascade to others.

Example scenario:

  1. Test A creates a user "testuser@example.com"
  2. Test A fails before cleanup
  3. Test B tries to create the same user → CONFLICT ERROR

The solution: Isolated test data

Database cleanup: Reset state before each test


Choosing the Right Tool: Detox vs WebDriverIO

We evaluated both for our React Native app. Here's what we learned:

FeatureDetoxWebDriverIO (Appium)
Synchronization✅ Automatic (React Native aware)⚠️ Manual waitFor() needed
Speed✅ Fast (gray box testing)❌ Slower (black box)
Flakiness✅ Very stable (~95%)⚠️ Requires careful waits (~80%)
Cross-platform✅ iOS + Android✅ iOS + Android + Web
Native apps❌ React Native / Native only✅ Any app
Setup complexity⚠️ Moderate (requires RN integration)⚠️ Moderate (Appium server setup)
Debugging✅ Excellent (logs, screenshots)✅ Good (requires config)

Our verdict:

  • Detox for React Native apps (our choice)
  • WebDriverIO for native iOS/Android or hybrid apps

Implementation: Detox Setup

1. Installation

2. Configuration

3. First Test

4. Running Tests


Advanced Patterns for Stability

Pattern 1: Page Object Model

Encapsulate screen interactions:

Usage in tests:


Pattern 2: Custom Matchers

Reduce test code duplication:


Pattern 3: Mocking Network Requests

Control API responses for deterministic tests:


CI/CD Integration

GitHub Actions Example


Performance Optimization

Before Optimization

  • Total execution time: 2 hours 15 minutes
  • Test count: 120 tests
  • Average per test: ~67 seconds

After Optimization

  • Total execution time: 18 minutes
  • Test count: 120 tests
  • Average per test: ~9 seconds

Improvements made:

  1. Parallel execution: Run tests across 4 simulators simultaneously

  2. Reuse app instances: Don't rebuild app between tests

  3. Disable animations in test builds:

  4. Skip unnecessary tests in smoke runs:


Debugging Tips

Take screenshots on failure

Enable detailed logs

Use Detox synchronization debug


Common Pitfalls & Solutions

PitfallSolution
Tests timeout waiting for elementAdd explicit waits: waitFor().withTimeout(10000)
Keyboard covers input fieldsAlways dismiss: element.tapReturnKey()
Different behavior iOS vs AndroidUse platform-specific conditionals: device.getPlatform()
Tests pass locally, fail in CIEnsure CI has same iOS version, use --headless mode
Random test failuresIncrease timeouts, add retries: jest.retryTimes(2)

Results & Takeaways

After 6 months of implementing these patterns:

MetricBeforeAfter
Test stability60%95%
Execution time2h 15min18min
Manual testing time3 days/release4 hours/release
Bug detection rate~70%~92%
Developer confidenceLowHigh

Key lessons:

  1. Invest in synchronization: Most flakiness comes from poor timing handling
  2. Enforce testID discipline: Make it a requirement for all interactive elements
  3. Isolate test data: Each test should be independent
  4. Use the right tool: Detox for React Native, WebDriverIO for native apps
  5. Optimize for CI: Parallel execution and smart test selection matter

Resources

Mobile automation doesn't have to be a nightmare. With the right tools and patterns, you can achieve the same stability as web automation.

Questions? Connect with me on LinkedIn.