How to Run Selenium Tests in Parallel with JUnit 5
Enable JUnit 5 parallel execution, isolate each Selenium WebDriver, and size concurrency safely. Includes Maven, local, and Grid setup plus fixes.
To run Selenium tests in parallel with JUnit 5, enable JUnit Jupiter parallel execution through JUnit Platform configuration, choose a bounded concurrency strategy, and create a separate WebDriver for each test thread. Add Selenium Grid when you need browsers on remote machines or broader browser and operating-system coverage. Start with a small concurrency limit: the safe number depends on runner resources, browser capacity, Grid limits, test-data isolation, and the application under test.
1. Enable parallel execution in JUnit 5
JUnit Jupiter parallel execution is opt-in. Put junit-platform.properties in src/test/resources so the test runtime can discover it. This example enables concurrent methods and uses a fixed, bounded pool of four threads:
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = 4
junit.jupiter.execution.parallel.config.fixed.max-pool-size = 4
Use a smaller number if your machine, CI worker, or browser environment has fewer resources. Fixed parallelism controls the intended concurrency; the maximum pool size bounds the pool. JUnit also supports dynamic and custom strategies. Dynamic parallelism derives its target from available processors and a configurable factor, but processor count alone does not account for browser memory, Grid session limits, or application capacity. See the JUnit Jupiter parallel execution guide for mode and strategy details.
Choose which tests may overlap
The default mode setting applies to the selected test-node level. You can configure class and method modes independently:
junit.jupiter.execution.parallel.mode.classes.default = concurrent
junit.jupiter.execution.parallel.mode.default = same_thread
This allows classes to run concurrently while methods within each class remain in the same thread. Conversely, concurrent methods with same-thread classes can be useful when one class contains independent methods. The right mode depends on fixture and data sharing. Tests that mutate shared state, use the same account, or depend on ordering need isolation or explicit coordination before they can safely overlap.
2. Configure Maven Surefire
With Maven Surefire, pass JUnit Platform parameters using configurationParameters. The following example uses the Selenium documentation’s Surefire 3.6.0 setup style and bounds the fixed pool through a Maven property:
<properties>
<selenium.parallelism>4</selenium.parallelism>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<properties>
<configurationParameters>
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.config.strategy = fixed
junit.jupiter.execution.parallel.config.fixed.parallelism = ${selenium.parallelism}
junit.jupiter.execution.parallel.config.fixed.max-pool-size = ${selenium.parallelism}
</configurationParameters>
</properties>
</configuration>
</plugin>
</plugins>
</build>
Run the suite with mvn test; override the limit for a particular run with mvn test -Dselenium.parallelism=2. Ensure your project already includes the JUnit Jupiter engine and Selenium Java dependencies. The Selenium Java install guide shows a Maven setup and Surefire configuration parameters.
Surefire documentation has had wording that conflicts with JUnit Jupiter’s documented parallel execution and Selenium’s Maven example. For Jupiter, configure concurrency with JUnit Platform parameters, then verify the exact Surefire version and provider your project uses. Surefire says that since version 3.6.0 tests run through the JUnit Platform provider; avoid assuming Maven’s generic parallel option is the control for Jupiter concurrency. See the Surefire JUnit Platform documentation.
3. Give each test its own WebDriver
Never let concurrently executing tests operate on the same WebDriver. A simple pattern creates one driver per test and quits it in teardown:
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ThreadGuard;
class ProductPageTest {
private WebDriver driver;
@BeforeEach
void setUp() {
driver = ThreadGuard.protect(new ChromeDriver());
}
@AfterEach
void tearDown() {
if (driver != null) {
driver.quit();
}
}
@Test
void opensProductPage() {
driver.get("https://example.com/product");
// Add assertions for the page under test.
}
}
For a shared JUnit extension or base class, a ThreadLocal<WebDriver> can associate a driver with the executing thread. Always call quit() and then remove() during teardown, including when setup or assertions fail. Do not keep a driver in a static field shared by parallel tests.
private static final ThreadLocal<WebDriver> DRIVER = new ThreadLocal<>();
@BeforeEach
void setUp() {
DRIVER.set(ThreadGuard.protect(new ChromeDriver()));
}
WebDriver driver() {
return DRIVER.get();
}
@AfterEach
void tearDown() {
WebDriver current = DRIVER.get();
try {
if (current != null) {
current.quit();
}
} finally {
DRIVER.remove();
}
}
ThreadGuard detects calls made to a protected driver from a thread other than the one that created it. It helps expose accidental cross-thread access; it does not make a shared driver safe or replace per-thread driver management. See Selenium ThreadGuard.
4. Prevent shared test data and fixture conflicts
Separate drivers prevent browser-session collisions, but parallel tests can still interfere through the application or test code. Before raising the worker count, check the following:
- Give each test unique users, records, files, and other mutable test data.
- Avoid mutable static fields and shared page objects that retain browser state.
- Do not depend on execution order or on one test cleaning up state for another.
- Use isolated schemas, tenants, or namespaces where tests write to shared services.
- Keep setup and teardown safe when a test fails partway through.
- Use JUnit resource locks or same-thread execution for genuinely shared resources that cannot be isolated.
Parallel tests increase pressure on the system under test too. If the application rate-limits logins or has limited test accounts, more browser workers can cause failures that look like Selenium problems.
5. Use Selenium Grid for remote parallel browsers
JUnit parallel execution schedules work in the test process; it does not distribute browsers to remote machines. Selenium Grid routes WebDriver commands to remote browser instances and supports parallel runs across machines, browser versions, and operating systems. Selenium puts it plainly: “Want to run tests in parallel across multiple machines? Then, Grid is for you.” See the Selenium Grid overview.
For a local standalone Grid, Selenium’s getting-started guide calls for Java 11 or higher, a Selenium Server JAR, and browsers and drivers (or Selenium Manager). Start the server with the versioned JAR filename:
java -jar selenium-server-<version>.jar standalone
Point each test’s remote driver at http://localhost:4444:
import java.net.URL;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
URL grid = new URL("http://localhost:4444");
WebDriver driver = new RemoteWebDriver(grid, new ChromeOptions());
Use the same per-test lifecycle and isolation rules with RemoteWebDriver. In CI, the URL will usually be the reachable Grid endpoint for that job or environment. The Selenium Grid getting-started guide covers standalone startup and prerequisites.
Local concurrency or Grid?
| Factor | Local parallel browsers | Selenium Grid |
|---|---|---|
| Setup and operations | Simpler; no Grid service to operate. | Requires Grid deployment, capacity management, and reachable nodes. |
| Where browsers run | On the test runner’s machine. | On remote nodes, potentially across machines. |
| Browser and OS coverage | Limited to what the runner has installed. | Can route tests to configured browser and platform combinations. |
| Capacity | Bound by runner CPU, memory, and browser stability. | Bound by available Grid nodes, slots, and their resources. |
| Best fit | Small suites and teams validating concurrency locally. | Remote execution, broader coverage, or more distributed capacity. |
| Cost and CI limits | Consumes runner resources and may hit CI worker limits. | Consumes Grid infrastructure or hosted capacity; account for queueing and session limits. |
6. Size concurrency from capacity
Do not equate JUnit’s thread count with the number of useful browser sessions. The practical limit is the lowest cap among JUnit workers, CI capacity, local resources, Grid slots, available test data, and application limits.
Selenium’s current Grid guidance says a Node’s default maximum session count is limited by CPU count, with Safari limited to one session in its documented example. It estimates around 1 GB of RAM per browser session and recommends smaller Nodes for process isolation. These are Selenium documentation guidance figures, not universal sizing guarantees; browser versions, workloads, and machine configuration matter. See the Grid documentation.
Increase the concurrency limit gradually. Record suite duration, browser startup failures, timeouts, memory use, Grid queueing, and application errors at each step. Stop increasing when the extra sessions cause instability or no longer improve elapsed time. Selenium’s Grid applicability page gives hypothetical arithmetic examples: 15 tests taking 45 seconds each are shown as 11 minutes 15 seconds on one node, 2 minutes 15 seconds across five, and 45 seconds across 15. Those are illustrations of ideal distribution, not measured performance results or a promise for a real suite.
7. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Tests still run one at a time | Parallel execution is not enabled, the properties file is misplaced, or Surefire is not passing the JUnit Platform parameters. | Confirm junit.jupiter.execution.parallel.enabled=true, place the file under src/test/resources, and inspect the Surefire version/provider and effective configuration. |
| Wrong-thread or ThreadGuard exception | A driver created on one thread is being used from another, often because it is shared statically or captured by asynchronous work. | Create one driver per test/thread; keep all calls on its owning thread and do not pass it to parallel callbacks. |
| Browser sessions leak or the machine runs out of memory | Drivers are not quit on all paths, or concurrency exceeds available resources. | Put quit() in teardown/finally logic, remove ThreadLocal values, and lower the pool or Grid session count. |
| Grid reports no matching slot or session creation fails | The requested browser capability is unavailable, all matching slots are busy, or the endpoint is unreachable. | Check Grid URL, node registration, browser capabilities, slot availability, and requested session count. |
| Intermittent failures appear only in parallel mode | Tests share accounts, records, files, static fixtures, or rate-limited application resources. | Isolate data and fixtures, or constrain those tests to same-thread execution/use a JUnit resource lock. |
| More workers make the suite slower | CPU or memory contention, browser startup overhead, Grid queueing, or application bottlenecks outweigh parallel work. | Measure at lower limits, identify the saturated resource, and set concurrency below the point where contention rises. |
| Tests pass locally but fail in CI | CI has fewer resources, different browser availability, a different Surefire/JUnit runtime, or a shared environment. | Log dependency and browser versions, verify the CI worker/session limits, and tune the concurrency property for that environment. |
Or skip the browser setup
If the task is taking a screenshot of a page rather than exercising browser interactions and assertions, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request returns an image or PDF; it does not replace Selenium for functional tests. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month, no card required.
Frequently asked questions
Does JUnit 5 parallel execution require Selenium Grid?
No. JUnit can run tests concurrently on one machine. Grid is for routing sessions to remote browser nodes and expanding machine or browser coverage.
Should I parallelize test classes or test methods?
Choose the level where tests are independent. Class-level concurrency with same-thread methods is a conservative start; enable method concurrency only when fixtures and data are isolated.
Can I use one WebDriver for an entire test class?
Only if the class’s tests are guaranteed not to overlap and the driver stays on its owning thread. A separate driver per test is easier to reason about under parallel execution.
Is a fixed pool always better than a dynamic pool?
No. Fixed sizing gives a predictable bound, useful for browser resource planning. Dynamic sizing may suit other workloads, but it does not know your Grid capacity or application limits.


