Parallel Execution & Distributed Testing Lab
(Java + Selenium + TestNG + Docker)
TL;DR – Parallel execution cuts test time from hours to minutes.
TestNG gives you fine‑grained control, Docker makes a reproducible grid, and a little thread‑safety hygiene goes a long way.
1. Fundamentals
| Concept | Why it matters | Quick example |
|---|---|---|
| Parallelism | Runs tests concurrently → faster feedback | parallel="methods" in TestNG |
| Isolation | Tests must not share mutable state | ThreadLocal<WebDriver> |
| Thread‑Safety | Avoid static fields unless immutable | @BeforeMethod creates a new driver |
| Scalability | Grid lets you spin up many nodes | Docker Compose = 10 Chrome nodes |
| Reporting | Know who failed and why | TestNG XML + Allure |
Tip – Think of a test run as a race; every shared resource is a potential crash point. Keep them off the track.
2. Set up Parallel Test Execution
2.1 Maven Project Skeleton
<!-- pom.xml -->
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>parallel-tests</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- Selenium -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.21.0</version>
</dependency>
<!-- TestNG -->
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.6.1</version>
<scope>test</scope>
</dependency>
<!-- WebDriverManager (auto driver binaries) -->
<dependency>
<groupId>io.github.bonigarcia</groupId>
<artifactId>webdrivermanager</artifactId>
<version>2.12.1</version>
<scope>test</scope>
</dependency>
<!-- Allure (optional) -->
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-testng</artifactId>
<version>2.21.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<suiteXmlFiles>
<suiteXmlFile>testng.xml</suiteXmlFile>
</suiteXmlFiles>
</configuration>
</plugin>
</plugins>
</build>
</project>
Why Maven? – It gives reproducible builds, easy dependency management, and integrates with CI.
2.2 Basic Test Class
package com.example.tests;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class SimpleSearchTest {
private WebDriver driver;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
}
@Test
public void verifyGoogleTitle() {
driver.get("https://www.google.com");
Assert.assertEquals(driver.getTitle(), "Google");
}
@AfterMethod
public void tearDown() {
if (driver != null) driver.quit();
}
}
Note – This is not parallel yet; each test will run sequentially.
3. Configure TestNG Parallel Modes
TestNG offers 4 parallel modes:
| Mode | What is executed in parallel | Typical use‑case |
|---|---|---|
| methods | All @Test methods in a test class | When tests are independent |
| tests | All <test> tags in testng.xml | Separate test suites (e.g., smoke vs regression) |
| classes | All test classes | When you want each class to run on its own thread |
| instances | Each test instance | For data‑driven tests that create new instances |
3.1 TestNG XML Example
<!-- testng.xml -->
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="ParallelSuite" parallel="methods" thread-count="8">
<test name="SmokeTests">
<classes>
<class name="com.example.tests.SimpleSearchTest"/>
<class name="com.example.tests.LoginTest"/>
</classes>
</test>
</suite>
Key points
thread-countcontrols how many threads TestNG may spawn.parallel="methods"is the most common for Selenium – each test method gets its own thread.- Caution – Do not share
WebDriverorThreadLocalobjects across tests unless you manage them carefully.
3.2 DataProvider Parallelism
@Test(dataProvider = "browsers")
@DataProvider(parallel = true)
public void runOnAllBrowsers(String browser) {
// ...
}
Tip –
parallel = truein@DataProviderensures each data set runs in a separate thread.
4. Implement Selenium Grid with Docker
4.1 What is Selenium Grid?
- Hub – Central point that receives test requests.
- Node – Browser instance that actually executes the test.
- Hub + Node can be on the same machine or distributed across clusters.
4.2 Docker Images (Official)
| Image | Purpose | Tag |
|---|---|---|
selenium/hub | Hub | 4.21.0 |
selenium/node-chrome | Chrome node | 4.21.0 |
selenium/node-firefox | Firefox node | 4.21.0 |
selenium/node-edge | Edge node | 4.21.0 |
Why Docker? – Consistency, isolation, easy teardown.
4.3 Docker‑Compose File
# docker-compose.yml
version: '3.8'
services:
selenium-hub:
image: selenium/hub:4.21.0
container_name: selenium-hub
ports:
- "4444:4444"
chrome:
image: selenium/node-chrome:4.21.0
container_name: chrome-node
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PORT=4442
- SE_EVENT_BUS_QUEUE=selenium-hub
volumes:
- /dev/shm:/dev/shm
firefox:
image: selenium/node-firefox:4.21.0
container_name: firefox-node
depends_on:
- selenium-hub
environment:
- SE_EVENT_BUS_HOST=selenium-hub
- SE_EVENT_BUS_PORT=4442
- SE_EVENT_BUS_QUEUE=selenium-hub
volumes:
- /dev/shm:/dev/shm
Why
/dev/shm– Increases shared memory for Chrome/Firefox to avoid crashes.
4.4 Launch the Grid
docker compose up -d
You should see logs indicating the hub is listening on 4444 and nodes are registered.
4.5 Verify Registration
Open http://localhost:4444/grid/console – you’ll see the nodes listed.
5. Execute Tests Across Multiple Browsers
5.1 Base Test Class (RemoteWebDriver)
package com.example.tests;
import io.github.bonigarcia.webdrivermanager.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Parameters;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
public abstract class BaseRemoteTest {
protected ThreadLocal<WebDriver> driver = new ThreadLocal<>();
@BeforeMethod
@Parameters("browser")
public void setUp(String browser) throws Exception {
Map<String, Object> capabilities = new HashMap<>();
switch (browser.toLowerCase()) {
case "chrome":
capabilities.put("browserName", "chrome");
break;
case "firefox":
capabilities.put("browserName", "firefox");
break;
case "edge":
capabilities.put("browserName", "MicrosoftEdge");
break;
default:
throw new IllegalArgumentException("Unsupported browser: " + browser);
}
driver.set(new RemoteWebDriver(
new URL("http://localhost:4444/wd/hub"),
capabilities));
}
protected WebDriver getDriver() {
return driver.get();
}
@AfterMethod
public void tearDown() {
if (driver.get() != null) driver.get().quit();
}
}
5.2 Test Class Using the Base
package com.example.tests;
import org.testng.Assert;
import org.testng.annotations.Test;
public class CrossBrowserSearchTest extends BaseRemoteTest {
@Test
public void googleTitle() {
getDriver().get("https://www.google.com");
Assert.assertEquals(getDriver().getTitle(), "Google");
}
}
5.3 Running Across All Browsers
Create a testng.xml with a <parameter> for each browser.
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="CrossBrowserSuite" parallel="tests" thread-count="3">
<test name="ChromeTest">
<parameter name="browser" value="chrome"/>
<classes>
<class name="com.example.tests.CrossBrowserSearchTest"/>
</classes>
</test>
<test name="FirefoxTest">
<parameter name="browser" value="firefox"/>
<classes>
<class name="com.example.tests.CrossBrowserSearchTest"/>
</classes>
</test>
<test name="EdgeTest">
<parameter name="browser" value="edge"/>
<classes>
<class name="com.example.tests.CrossBrowserSearchTest"/>
</classes>
</test>
</suite>
Result – Three parallel test runs, each on a different browser node.
6. Analyze Parallel Execution Results
6.1 TestNG Reports
- XML –
test-output/testng-results.xmlcontains per‑test status, duration, thread ID. - HTML –
test-output/index.htmlis a quick‑look dashboard. - Allure – For richer visualizations; run
allure serve target/allure-results.
6.2 Logging & Screenshots
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class LoggerUtil {
private static final Logger logger = LogManager.getLogger(LoggerUtil.class.getName());
public static void log(String msg) {
logger.info(msg);
}
}
Add LoggerUtil.log("Thread: " + Thread.currentThread().getName()); inside tests to trace which thread ran what.
6.3 Performance Metrics
| Metric | How to collect | Tool |
|---|---|---|
| Execution time | @AfterMethod record System.nanoTime() | TestNG |
| Throughput | Count of tests per second | Custom listener |
| Resource usage | Docker stats (docker stats) | Prometheus + Grafana |
Pro tip – In CI, use
-Dtestng.parallel=methods -Dtestng.threadcount=8to override XML at runtime.
6.4 Debugging Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
java.lang.IllegalStateException | Driver already quit | Ensure @AfterMethod runs after each test |
java.net.ConnectException | Hub unreachable | Verify hub URL, network, port 4444 |
org.openqa.selenium.WebDriverException: timeout | Node busy | Increase thread-count or use grid’s maxSession setting |
java.lang.AssertionError | Shared static data | Use ThreadLocal or fresh objects per test |
7. Advanced Topics
| Topic | Why it matters | Practical snippet |
|---|---|---|
| ThreadLocal | Keeps driver isolated | private ThreadLocal<WebDriver> driver = new ThreadLocal<>(); |
| TestNG Listeners | Capture logs, screenshots on failure | @Listeners({AllureTestNGListener.class}) |
| DataProvider Parallel | Run data sets in parallel | @DataProvider(parallel = true) |
| Selenium Grid 4 | Supports multiple hubs, self‑healing nodes | -Dselenium.hub.port=4444 |
| Docker Swarm | Scale grid automatically | docker stack deploy -c docker-compose.yml grid |
| CI Integration | GitHub Actions, Jenkins | mvn test -Dsurefire.suiteXmlFiles=testng.xml |
| Performance Regression | Detect slowdowns | Compare durations across runs |
8. Real‑World Applications
| Scenario | How Parallelism Helps | Implementation Notes |
|---|---|---|
| E‑commerce multi‑locale | Run tests for US, EU, Asia in one run | Use @DataProvider with locale data |
| Mobile Web | Use Appium nodes in Grid | Same hub, different node images |
| Cross‑device | Parallel on iOS, Android, Desktop | Docker‑Compose with appium/node-chrome etc |
| Regression suite | 2000 tests → 30 min | Set thread-count=16 and use parallel=classes |
| CI Pipeline | Run tests on PR merge | GitHub Actions workflow with Docker‑Compose |
Industry Insight – Large companies (e.g., Amazon, Shopify) run ~50,000 tests nightly. They rely on TestNG + Docker Grid + CI to keep it under 30 min.
9. Exercises
| # | Task | Deliverable |
|---|---|---|
| 1 | Create a Maven project with Selenium + TestNG | pom.xml |
| 2 | Write a test that opens https://example.com and verifies the header | ExampleTest.java |
| 3 | Configure testng.xml to run 4 methods in parallel | testng.xml |
| 4 | Spin up Selenium Grid via Docker Compose (hub + 2 nodes) | docker-compose.yml |
| 5 | Run the test against the Grid on Chrome & Firefox | Verify logs show different thread IDs |
| 6 | Add a TestNG listener that logs start/end times | TimingListener.java |
| 7 | Refactor tests to use ThreadLocal<WebDriver> | Updated test classes |
| 8 | Create a custom Docker image that pre‑installs ChromeDriver | Dockerfile |
| 9 | Integrate Allure reports into Maven build | allure-results folder |
| 10 | Simulate a failure (e.g., wrong title) and capture screenshot | screenshots/ |
Challenge – Scale the grid to 10 Chrome nodes and run a 200‑test regression suite in under 20 min. Report the throughput and identify bottlenecks.
10. Professional Tips & Industry Insights
| Tip | Why it matters |
|---|---|
| Keep tests stateless | Avoid flaky tests |
Use ThreadLocal for WebDriver | Each thread gets its own driver |
Limit thread-count to available CPU cores | Prevent oversubscription |
Add -Dwebdriver.remote.sessionid | Helps debugging node assignments |
Use Docker Compose overrides (docker-compose.override.yml) for CI vs local | Keeps environments consistent |
Prefer selenium/hub:4.21.0 over older images | Newer APIs, better logs |
Enable max-sessions on hub | Prevent node starvation |
Run docker compose down -v after tests | Clean volumes & data |
Bottom line – Parallel execution is a power‑tool; misuse can break your suite. Start simple, test thoroughly, then scale.
References
- Selenium 4 Guide – 2025
- TestNG User Guide
- Docker Selenium Grid Official Docs
- WebDriverManager
- Allure TestNG
Happy testing! 🚀