Skip to main content
L3gitUI & API TEST AUTOMATION
[ PRODUCT ][ PRICING ][ DOCS ][ VERIFIED ][ REGULATED ][ COMPARE ][ ACCOUNT ] Start Free Trial

/// Resources / Jenkins

How to run parallel Selenium tests in Jenkins without Selenium Grid

Published 2026-11-057 min readL3git Development

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.

Matrix axes must be literal in the Jenkinsfile. They cannot come from a build parameter, which is why L3git writes the file from the shard and browser choices you make at generate time.

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

  1. JDK 17 and Maven on the path.
  2. 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.
  3. The label from the Jenkinsfile (ui-test here) 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.

Try it: 14-day trial, no card More articles