Running 200 tests sequentially against a single emulator takes 45 minutes. Running the same tests across 5 devices simultaneously takes 9 minutes. Parallel execution is not just a nice-to-have — at a certain test suite size, it is the difference between feedback that arrives before the developer moves on and feedback that arrives the next morning. This lesson shows how to parallelise Appium tests with TestNG, handle thread safety, and scale to multiple devices.
The fundamental rule: one driver per thread
Every parallel thread must own an exclusive AppiumDriver instance. A driver instance wraps an Appium session which wraps a single device connection. Sharing a driver across threads causes commands from different tests to interleave on the same device — tests fail in unpredictable ways.
Naive parallel setup that breaks:
// WRONG — static shared driver
public class BaseTest {
protected static AndroidDriver driver; // shared across all threads
}Thread-safe setup using ThreadLocal:
public class BaseTest {
private static final ThreadLocal<AndroidDriver> driverThread = new ThreadLocal<>();
protected AndroidDriver getDriver() {
return driverThread.get();
}
@BeforeMethod
public void setUp() throws Exception {
AndroidDriver driver = DriverFactory.createAndroidDriver();
driverThread.set(driver);
}
@AfterMethod
public void tearDown() {
AndroidDriver driver = driverThread.get();
if (driver != null) {
driver.quit();
driverThread.remove();
}
}
}ThreadLocal gives each thread its own driver reference. driverThread.get() in a test method returns the driver that was created by that thread's @BeforeMethod.
TestNG parallel configuration
TestNG controls parallelism through testng.xml. The parallel attribute can be set to methods, classes, or tests.
Parallel at the methods level (recommended for small suites)
Each test method gets its own thread and therefore its own driver:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Parallel Suite" parallel="methods" thread-count="5">
<test name="Login Tests">
<classes>
<class name="com.qa.tests.LoginTest" />
<class name="com.qa.tests.CheckoutTest" />
<class name="com.qa.tests.ProfileTest" />
</classes>
</test>
</suite>thread-count="5" means up to 5 tests run simultaneously. For cloud providers, this translates to 5 concurrent device sessions.
Parallel at the tests level (for different platforms)
Run Android and iOS tests simultaneously as separate test groups:
<suite name="Cross-Platform Suite" parallel="tests" thread-count="2">
<test name="Android Tests">
<parameter name="platform" value="android"/>
<classes>
<class name="com.qa.tests.LoginTest" />
</classes>
</test>
<test name="iOS Tests">
<parameter name="platform" value="ios"/>
<classes>
<class name="com.qa.tests.LoginTest" />
</classes>
</test>
</suite>Read the platform parameter in BaseTest:
@Parameters({"platform"})
@BeforeMethod
public void setUp(String platform) throws Exception {
AppiumDriver driver;
if ("ios".equalsIgnoreCase(platform)) {
driver = DriverFactory.createIOSDriver();
} else {
driver = DriverFactory.createAndroidDriver();
}
driverThread.set(driver);
}Running against multiple device configurations
To run the same tests against multiple Android versions simultaneously, use TestNG data providers with capabilities:
@DataProvider(name = "deviceConfigs", parallel = true)
public Object[][] deviceConfigs() {
return new Object[][]{
{"emulator-5554", "12"},
{"emulator-5556", "13"},
{"emulator-5558", "14"},
};
}
@Test(dataProvider = "deviceConfigs")
public void loginTest(String deviceName, String platformVersion) throws Exception {
UiAutomator2Options options = new UiAutomator2Options()
.setDeviceName(deviceName)
.setPlatformVersion(platformVersion)
.setApp("/path/to/app.apk");
AndroidDriver localDriver = new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
try {
localDriver.findElement(AppiumBy.accessibilityId("login_button")).click();
// assertions...
} finally {
localDriver.quit();
}
}Running multiple Appium servers locally
When running parallel tests against multiple local devices, you need a separate Appium server per device to avoid port conflicts:
# Start 3 servers on different ports
appium --port 4723 &
appium --port 4724 &
appium --port 4725 &Then point each driver to the appropriate port:
// Thread 1 → port 4723
new AndroidDriver(new URL("http://127.0.0.1:4723"), options);
// Thread 2 → port 4724
new AndroidDriver(new URL("http://127.0.0.1:4724"), options);In practice, dynamic port assignment from a pool is cleaner than hardcoding:
private static final AtomicInteger portCounter = new AtomicInteger(4723);
public static URL getNextAppiumUrl() throws MalformedURLException {
return new URL("http://127.0.0.1:" + portCounter.getAndIncrement());
}Parallel execution on cloud providers
Cloud providers run all sessions in their infrastructure, so you don't manage ports or Appium server instances. Just increase thread-count in testng.xml and ensure your cloud plan supports the concurrency level:
<suite name="BrowserStack Suite" parallel="methods" thread-count="10">This starts up to 10 simultaneous device sessions on BrowserStack. Check your plan's concurrency limit — most plans cap at 5 or 10 parallel sessions.
Avoiding shared state
When tests run in parallel, any shared state causes flakiness:
- Static fields in test classes — replace with
ThreadLocal - Shared test data files — use unique user accounts per thread or generate test data programmatically
- Shared Appium server logs — each test run should write to a separate log file
- Assertions on shared counters — isolate each test's data set
Thread-safe reporting
TestNG's default reporters are thread-safe. Extent Reports and Allure Reports both support parallel test runs — consult their documentation for thread-safe reporter setup.
The key pattern: create a reporter context per thread, write to it during the test, and flush it in @AfterMethod:
private static final ThreadLocal<ExtentTest> extentTest = new ThreadLocal<>();
@BeforeMethod
public void beforeMethod(Method method) {
extentTest.set(extent.createTest(method.getName()));
}