Selenium Grid & Parallel Execution

The RecipeBox regression suite now has 20 data-driven "Add Recipe" tests plus everything from earlier lessons — login, search, dropdowns, alerts. Run that one test at a time, on one browser, and it takes forever. Selenium Grid and TestNG parallel execution solve two related but different problems: running across multiple browsers/OS combinations, and simply running many tests at the same time instead of one after another.

What Selenium Grid Actually Is

Selenium Grid is a hub-and-node setup. Your test code talks to one central hub; the hub forwards each test's session request to a registered node — a machine (or container) running an actual browser. This lets you:

  • Run the same test against Chrome, Firefox, and Edge at once, on different nodes.
  • Run many tests simultaneously across several nodes instead of queuing them on one machine.
  • Keep the machine that runs tests separate from the machines that execute browsers.
PieceRole
HubReceives test requests, matches them to a node with a matching browser/OS, and routes WebDriver commands to it.
NodeA machine/container with real browsers installed. Actually executes the clicks, typing, navigation.
Your test codePoints RemoteWebDriver at the hub's URL instead of launching a local browser directly.

Connecting to a Grid hub instead of a local browser is a small code change — swap the driver:

URL hubUrl = new URL("http://localhost:4444/wd/hub");

ChromeOptions options = new ChromeOptions();
WebDriver driver = new RemoteWebDriver(hubUrl, options);

driver.get("https://recipebox.example.com/login");
Note: Standing up an actual Grid hub and nodes is an infrastructure task (Docker images are the common route today) — this lesson focuses on the concept and the WebDriver-side code, since the server setup is environment-specific.

Parallel Execution with TestNG

Separately from Grid, TestNG can run multiple @Test methods (or whole classes) concurrently on your local machine, each launching its own browser instance. This is controlled in testng.xml via the parallel attribute:

<suite name="RecipeBoxRegressionSuite" parallel="methods" thread-count="4">
    <test name="AddRecipeTests">
        <classes>
            <class name="tests.AddRecipeTest"/>
            <class name="tests.SearchRecipeTest"/>
        </classes>
    </test>
</suite>

parallel="methods" with thread-count="4" tells TestNG to run up to 4 @Test methods at the same time, across threads. Your 20 data-driven testAddRecipe invocations from the previous lesson can now run 4 at a time instead of one at a time.

parallel= valueBehavior
"methods"Each @Test method runs on its own thread.
"classes"Each test class runs on its own thread; methods within a class stay sequential.
"tests"Each <test> tag in the XML runs on its own thread.

The Thread-Safety Trap: One Shared WebDriver

The single most common parallel-execution bug: declaring one static WebDriver driver field and letting every thread reuse it. Two threads calling driver.get(...) on the same browser session at the same time produces flaky, nonsensical failures — one test's clicks land on another test's page.
// WRONG — do not share one driver instance across parallel threads
public class BaseTest {
    public static WebDriver driver; // every thread fights over this one instance
}

The Fix: ThreadLocal<WebDriver>

ThreadLocal gives each thread its own private copy of a variable. Every parallel TestNG thread gets its own WebDriver instance, own browser window, own session — no cross-talk.

public class BaseTest {

    protected static ThreadLocal<WebDriver> driverThread = new ThreadLocal<>();

    @BeforeMethod
    public void setUp() {
        ChromeOptions options = new ChromeOptions();
        WebDriver driver = new ChromeDriver(options);
        driverThread.set(driver); // stored per-thread, not shared
    }

    protected WebDriver getDriver() {
        return driverThread.get();
    }

    @AfterMethod
    public void tearDown() {
        getDriver().quit();
        driverThread.remove(); // avoid leaking the thread's driver reference
    }
}
public class AddRecipeTest extends BaseTest {

    @Test(dataProvider = "recipeData")
    public void testAddRecipe(String title, String category, int prepTime) {
        WebDriver driver = getDriver(); // this thread's own instance
        AddRecipePage addRecipePage = new AddRecipePage(driver);
        addRecipePage.enterTitle(title);
        addRecipePage.selectCategory(category);
        addRecipePage.enterPrepTime(prepTime);
        addRecipePage.save();

        Assert.assertTrue(addRecipePage.isRecipeSaved(title),
            "Recipe '" + title + "' did not save successfully");
    }
}
Rule of thumb: the moment parallel appears in your testng.xml, every static mutable field in your base test class is a potential race condition. ThreadLocal is the standard fix for WebDriver specifically, but the same instinct applies to any shared state.
Next: a parallel suite that only runs when you remember to run it manually still isn't good enough. Head to CI Basics with Jenkins to trigger it automatically on every RecipeBox pull request.