Parallel Execution & Distributed Testing Lab

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

ConceptWhy it mattersQuick example
ParallelismRuns tests concurrently → faster feedbackparallel="methods" in TestNG
IsolationTests must not share mutable stateThreadLocal<WebDriver>
Thread‑SafetyAvoid static fields unless immutable@BeforeMethod creates a new driver
ScalabilityGrid lets you spin up many nodesDocker Compose = 10 Chrome nodes
ReportingKnow who failed and whyTestNG 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:

ModeWhat is executed in parallelTypical use‑case
methodsAll @Test methods in a test classWhen tests are independent
testsAll <test> tags in testng.xmlSeparate test suites (e.g., smoke vs regression)
classesAll test classesWhen you want each class to run on its own thread
instancesEach test instanceFor 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-count controls 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 WebDriver or ThreadLocal objects across tests unless you manage them carefully.

3.2 DataProvider Parallelism

@Test(dataProvider = "browsers")
@DataProvider(parallel = true)
public void runOnAllBrowsers(String browser) {
    // ...
}

Tipparallel = true in @DataProvider ensures 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)

ImagePurposeTag
selenium/hubHub4.21.0
selenium/node-chromeChrome node4.21.0
selenium/node-firefoxFirefox node4.21.0
selenium/node-edgeEdge node4.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

  • XMLtest-output/testng-results.xml contains per‑test status, duration, thread ID.
  • HTMLtest-output/index.html is 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

MetricHow to collectTool
Execution time@AfterMethod record System.nanoTime()TestNG
ThroughputCount of tests per secondCustom listener
Resource usageDocker stats (docker stats)Prometheus + Grafana

Pro tip – In CI, use -Dtestng.parallel=methods -Dtestng.threadcount=8 to override XML at runtime.

6.4 Debugging Common Issues

SymptomLikely CauseFix
java.lang.IllegalStateExceptionDriver already quitEnsure @AfterMethod runs after each test
java.net.ConnectExceptionHub unreachableVerify hub URL, network, port 4444
org.openqa.selenium.WebDriverException: timeoutNode busyIncrease thread-count or use grid’s maxSession setting
java.lang.AssertionErrorShared static dataUse ThreadLocal or fresh objects per test

7. Advanced Topics

TopicWhy it mattersPractical snippet
ThreadLocalKeeps driver isolatedprivate ThreadLocal<WebDriver> driver = new ThreadLocal<>();
TestNG ListenersCapture logs, screenshots on failure@Listeners({AllureTestNGListener.class})
DataProvider ParallelRun data sets in parallel@DataProvider(parallel = true)
Selenium Grid 4Supports multiple hubs, self‑healing nodes-Dselenium.hub.port=4444
Docker SwarmScale grid automaticallydocker stack deploy -c docker-compose.yml grid
CI IntegrationGitHub Actions, Jenkinsmvn test -Dsurefire.suiteXmlFiles=testng.xml
Performance RegressionDetect slowdownsCompare durations across runs

8. Real‑World Applications

ScenarioHow Parallelism HelpsImplementation Notes
E‑commerce multi‑localeRun tests for US, EU, Asia in one runUse @DataProvider with locale data
Mobile WebUse Appium nodes in GridSame hub, different node images
Cross‑deviceParallel on iOS, Android, DesktopDocker‑Compose with appium/node-chrome etc
Regression suite2000 tests → 30 minSet thread-count=16 and use parallel=classes
CI PipelineRun tests on PR mergeGitHub 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

#TaskDeliverable
1Create a Maven project with Selenium + TestNGpom.xml
2Write a test that opens https://example.com and verifies the headerExampleTest.java
3Configure testng.xml to run 4 methods in paralleltestng.xml
4Spin up Selenium Grid via Docker Compose (hub + 2 nodes)docker-compose.yml
5Run the test against the Grid on Chrome & FirefoxVerify logs show different thread IDs
6Add a TestNG listener that logs start/end timesTimingListener.java
7Refactor tests to use ThreadLocal<WebDriver>Updated test classes
8Create a custom Docker image that pre‑installs ChromeDriverDockerfile
9Integrate Allure reports into Maven buildallure-results folder
10Simulate a failure (e.g., wrong title) and capture screenshotscreenshots/

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

TipWhy it matters
Keep tests statelessAvoid flaky tests
Use ThreadLocal for WebDriverEach thread gets its own driver
Limit thread-count to available CPU coresPrevent oversubscription
Add -Dwebdriver.remote.sessionidHelps debugging node assignments
Use Docker Compose overrides (docker-compose.override.yml) for CI vs localKeeps environments consistent
Prefer selenium/hub:4.21.0 over older imagesNewer APIs, better logs
Enable max-sessions on hubPrevent node starvation
Run docker compose down -v after testsClean volumes & data

Bottom line – Parallel execution is a power‑tool; misuse can break your suite. Start simple, test thoroughly, then scale.


References

Happy testing! 🚀