Parallel execution is where TestNG outperforms JUnit 5 for mobile suites out of the box. One XML configuration file controls parallelism across tests, classes, and methods — and combined with testng.xml parameter injection, it's the standard way to run the same tests on Android and iOS simultaneously.
Parallelism levels
TestNG supports four parallel modes:
| Mode | What runs in parallel |
|---|---|
suite | Multiple <suite-files> in a master suite |
tests | Multiple <test> blocks within one suite |
classes | Multiple test classes within a <test> block |
methods | Individual test methods across classes |
For mobile cross-platform testing, parallel="tests" is the most useful — each <test> block represents a platform (Android / iOS), and they run concurrently.
Cross-platform parallel configuration
<suite name="Mobile Suite" parallel="tests" thread-count="2">
<test name="Android Regression">
<parameter name="platform" value="Android"/>
<parameter name="deviceName" value="emulator-5554"/>
<classes>
<class name="com.example.tests.LoginTest"/>
<class name="com.example.tests.CheckoutTest"/>
<class name="com.example.tests.SearchTest"/>
</classes>
</test>
<test name="iOS Regression">
<parameter name="platform" value="iOS"/>
<parameter name="deviceName" value="iPhone 15"/>
<classes>
<class name="com.example.tests.LoginTest"/>
<class name="com.example.tests.CheckoutTest"/>
<class name="com.example.tests.SearchTest"/>
</classes>
</test>
</suite>thread-count="2" means two <test> blocks run simultaneously — Android in one thread, iOS in another. Set this equal to the number of <test> blocks for maximum parallelism.
Device matrix — multiple devices per platform
Run the same tests on multiple Android versions simultaneously:
<suite name="Mobile Suite" parallel="tests" thread-count="4">
<test name="Android API 30">
<parameter name="platform" value="Android"/>
<parameter name="deviceName" value="Pixel_4_API_30"/>
<parameter name="platformVersion" value="11"/>
<classes><class name="com.example.tests.LoginTest"/></classes>
</test>
<test name="Android API 33">
<parameter name="platform" value="Android"/>
<parameter name="deviceName" value="Pixel_7_API_33"/>
<parameter name="platformVersion" value="13"/>
<classes><class name="com.example.tests.LoginTest"/></classes>
</test>
<test name="iOS 16">
<parameter name="platform" value="iOS"/>
<parameter name="deviceName" value="iPhone 14"/>
<parameter name="platformVersion" value="16.4"/>
<classes><class name="com.example.tests.LoginTest"/></classes>
</test>
<test name="iOS 17">
<parameter name="platform" value="iOS"/>
<parameter name="deviceName" value="iPhone 15"/>
<parameter name="platformVersion" value="17.2"/>
<classes><class name="com.example.tests.LoginTest"/></classes>
</test>
</suite>DriverManager receiving parameters
BaseTest receives testng.xml parameters through @Parameters in @BeforeTest:
@BeforeTest
@Parameters({"platform", "deviceName", "platformVersion"})
public void setUp(String platform, String deviceName,
@Optional("latest") String platformVersion) {
DriverManager.initDriver(platform, deviceName, platformVersion);
}@Optional("latest") provides a default value for optional parameters — if platformVersion isn't in testng.xml, it defaults to "latest".
Parallel methods within a test
For tests that don't share device state, parallel="methods" within a <test> block runs each test method in a separate thread on the same device:
<suite name="Mobile Suite" parallel="methods" thread-count="3">
<test name="Android">
<parameter name="platform" value="Android"/>
<classes>
<class name="com.example.tests.LoginTest"/>
</classes>
</test>
</suite>This requires a separate driver per method — the ThreadLocal<AppiumDriver> must be initialised at @BeforeMethod scope, not @BeforeTest:
@BeforeMethod
@Parameters("platform")
public void setUp(String platform) {
DriverManager.initDriver(platform); // creates a new driver for this thread
}
@AfterMethod
public void tearDown() {
DriverManager.quitDriver();
}Method-level parallelism requires multiple connected devices or emulator instances — each method's driver is a separate Appium session.
Smoke vs regression split
Use separate testng.xml files for different CI stages:
testng-smoke.xml — fast subset, runs on every PR:
<suite name="Smoke" parallel="tests" thread-count="2">
<test name="Android Smoke">
<parameter name="platform" value="Android"/>
<groups><run><include name="smoke"/></run></groups>
<classes><class name="com.example.tests.LoginTest"/></classes>
</test>
<test name="iOS Smoke">
<parameter name="platform" value="iOS"/>
<groups><run><include name="smoke"/></run></groups>
<classes><class name="com.example.tests.LoginTest"/></classes>
</test>
</suite>Mark tests with @Test(groups = "smoke") to include them in the smoke run.
testng-regression.xml — full suite, runs nightly:
<suite name="Regression" parallel="tests" thread-count="4">
<!-- all test blocks -->
</suite>Running from Maven
<!-- pom.xml -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>${suiteFile}</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>
</plugins>
</build># Run smoke suite
mvn test -DsuiteFile=testng-smoke.xml
# Run regression suite
mvn test -DsuiteFile=testng-regression.xmlThe ${suiteFile} property lets CI pass the file path as a command-line argument without modifying the POM.