/// Resources / Jenkins
How to run parallel Selenium tests in Jenkins without Selenium Grid
A Selenium Grid is one way to run tests in parallel. It is also a hub to patch, a node pool to keep version-matched, and a single point of failure. If you already have Jenkins agents, you can get the same parallelism with a declarative matrix pipeline and a few lines of Java that decide which scenarios each cell runs. Here is the complete pattern.
The idea: cells, not nodes
Jenkins declarative pipelines have a matrix block. You name axes and values; Jenkins runs the inner stages once per combination, in parallel, each on an agent that matches your label. For a test suite the two useful axes are SHARD (which slice of the suite) and BROWSER (which installed browser). Four shards and two browsers is eight cells running at once, each driving the browser installed on its own agent.
Step 1: a deterministic shard selector
The hard part of sharding is deciding which scenario goes where. Splitting by file name or by position breaks the moment someone adds a test: everything reshuffles and your flake history stops lining up. The fix is to hash a stable id on each scenario. L3git tags every generated scenario with @l3id_<12 hex> and ships ShardSelector, a one-file Java class that hashes the id (FNV-1a) modulo the shard count and prints a Cucumber tag expression:
$ java -cp target/test-classes com.example.support.ShardSelector 2/4
@l3id_3fa9c1e204bb or @l3id_91d07e5aa013 or @l3id_c44e0b7d2f10
Same input, same output, every run. Adding a scenario adds one tag to one shard and moves nothing else. Pass --balance if you would rather have perfectly even counts than stable assignment.
Step 2: the matrix Jenkinsfile
Trimmed to the parts that matter:
pipeline {
agent none
parameters { string(name: 'TAGS', defaultValue: '', description: 'optional Cucumber tag filter') }
stages { stage('Matrix') { matrix {
axes {
axis { name 'SHARD'; values '1', '2', '3', '4' }
axis { name 'BROWSER'; values 'edge', 'chrome' }
}
agent { label 'ui-test' }
environment { SHARDS = '4'; HEALING_BROWSER = "${BROWSER}"; HEALING_HEADLESS = 'true' }
stages { stage('Test') { steps { script {
sh 'mvn -B -ntp -q test-compile'
def shard = sh(script: "java -cp target/test-classes com.example.support.ShardSelector ${SHARD}/${SHARDS}", returnStdout: true).trim()
def expr = [params.TAGS?.trim(), shard].findAll { it }.collect { "(${it})" }.join(' and ')
sh "mvn -B -ntp test -Dcucumber.filter.tags=\"${expr}\""
} } } }
post { always { junit testResults: 'target/surefire-reports/*.xml', allowEmptyResults: true } }
} } }
}
Three things to notice. agent none at the top and agent { label } inside the matrix means every cell picks its own agent. HEALING_BROWSER is read by the framework’s driver hooks, so the same suite runs on whatever the axis says. And TAGS stays a normal build parameter: @smoke combines with the shard expression using and.
Step 3: prepare the agents
- JDK 17 and Maven on the path.
- The browsers you listed in the axis, installed by IT. No Selenium Manager download is needed; the framework prefers the installed browser and, for air-gapped agents, a WebDriver you point at with
HEALING_DRIVER_PATH. - The label from the Jenkinsfile (
ui-testhere) on each agent.
Step 4: results
Each cell publishes its own JUnit XML; Jenkins merges them into one Test Result page. Blue Ocean shows the matrix as a grid of green and red squares. A failed cell is retried on its own, so one flaky agent no longer takes the whole suite down the way a hub outage does.
When you still want a Grid
Many browser versions or operating systems from one job. That is what Grids and device clouds are for, and one property (grid.url) points the same suite at BrowserStack, Sauce Labs, LambdaTest or your own hub. For throughput on the browsers you actually ship, agents you already own are cheaper and quieter.
Want the file written for you? L3git’s startup window has a Jenkins matrix row — shards, browsers, agent label — and generates exactly this pipeline into every project. The plain-English setup is in the 7-step Jenkins guide; the engineer-level one is guide 13.
Questions
Does this work with GitHub Actions or GitLab CI?
Yes. Anything that can run N jobs with an index can call ShardSelector k/n the same way. The generated GitHub Actions workflow uses a matrix with a total count; raise it to add shards.
What about scenarios without an @l3id tag?
Legacy scenarios with no stable id all run on shard 1, so nothing is silently skipped. Regenerating from L3git adds the tag.
Can I run parallel inside one agent too?
Yes. Add -Dtest.forkCount=1C to the mvn line to use every core on each agent, and -Dtest.rerun=2 to retry flaky scenarios before failing the cell.