This post documents our experience setting up parallel mobile automation on a single macOS machine, running:
-
Android tests on a real device
-
iOS tests on a simulator
using WebdriverIO, Appium, and Cucumber, with a shared test codebase.
The intention is to highlight the challenges, root causes, and final approach that worked reliably both locally and in CI.
Objective
-
Execute Android and iOS tests simultaneously
-
Maintain one automation codebase
-
Support:
-
Android-only execution
-
iOS-only execution
-
Android + iOS parallel execution
-
-
Ensure the setup is pipeline-friendly
Key Challenges Encountered
1. Appium Port Conflicts
Running both platforms against a single Appium server caused session creation conflicts.
Resolution
Two separate Appium servers were used:
-
iOS → port
4723 -
Android → port
4725
appium -p 4723 --base-path /
appium -p 4725 --base-path /
2. Locator Strategy Mismatch Across Platforms
When using Promise.all() with shared page objects, Android-specific locators were executed on iOS sessions and vice versa.
Typical error:
Locator Strategy '-android uiautomator' is not supported for this session
Root Cause
Using driver.isAndroid does not work correctly in MultiRemote mode, because multiple sessions exist simultaneously.
Resolution
Explicitly scope commands to the correct session:
-
browser.android -
browser.ios
3. Failures When Only One Platform Is Available
When running only iOS (Android device disconnected), tests still attempted to connect to the Android Appium server, resulting in connection failures.
Resolution
Execution was made platform-aware, so only available platforms are used.
Final Working Architecture
Platform Selection via Environment Variable
PLATFORM=android npx wdio run wdio.conf.js
PLATFORM=ios npx wdio run wdio.conf.js
PLATFORM=both npx wdio run wdio.conf.js
This allows:
-
Local execution
-
CI pipeline execution
-
Selective platform runs
MultiRemote Configuration (Conditional)
Only the required platforms are initialized based on PLATFORM.
This prevents attempts to connect to non-running Appium servers.
Page Object Design (Platform Explicit)
class LoginPage {
android = {
nextButton: () =>
browser.android.$('android=new UiSelector().description("Next")')
};
ios = {
nextButton: () =>
browser.ios.$('~Next')
};
}
This avoids conditional logic inside element definitions and keeps platform handling explicit.
Safe Parallel Execution Helper
async function runOnPlatforms(actions) {
const tasks = [];
if (browser.android && actions.android) {
tasks.push(actions.android());
}
if (browser.ios && actions.ios) {
tasks.push(actions.ios());
}
await Promise.all(tasks);
}
Cucumber Step Implementation
When('User clicks Next', async () => {
await runOnPlatforms({
android: () => LoginPage.android.nextButton().click(),
ios: () => LoginPage.ios.nextButton().click()
});
});
This works safely for:
-
Android only
-
iOS only
-
Android + iOS together
Key Learnings
-
driver.isAndroidshould not be used in MultiRemote scenarios -
Always scope commands using
browser.androidandbrowser.ios -
Never assume both platforms are available during execution
-
Platform-aware execution is essential for CI stability
-
A single Mac can reliably run Android (real device) and iOS (simulator) in parallel
Outcome
-
Stable local parallel execution
-
Single shared automation framework
-
CI-ready configuration
-
Easy to extend to cloud providers in the future
Discussion
Curious to hear how others are approaching mobile automation at scale:
-
How are you handling parallel execution across Android and iOS?
-
Are you running real devices and simulators in CI, or using cloud providers?
-
How do you manage Appium server orchestration in parallel setups?
-
Do you prefer a single unified framework or separate platform-specific pipelines?
Would love to exchange ideas and learn from different approaches.