Testing in Jenkins
Declarative pipelines, parallel stages, agent control and JUnit reporting — how to run a modern test suite on the CI server you probably inherited.
1 min read · updated 19 September 2026
Jenkins is the CI server you inherit. It is also, once configured properly, extremely capable — total control over agents, an enormous plugin ecosystem, and the ability to run in environments no hosted platform can reach.
#A declarative pipeline
// Jenkinsfile
pipeline {
agent none // per-stage agents; no global executor held
options {
timeout(time: 45, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '10'))
disableConcurrentBuilds(abortPrevious: true)
timestamps()
ansiColor('xterm')
}
environment {
CI = 'true'
NODE_OPTIONS = '--max-old-space-size=4096'
}
stages {
stage('Checks') {
agent { docker { image 'node:22'; args '-v $HOME/.npm:/root/.npm' } }
steps {
sh 'npm ci'
sh 'npm run lint'
sh 'npm run typecheck'
}
}
stage('Tests') {
parallel {
stage('Unit') {
agent { docker { image 'node:22' } }
steps {
sh 'npm ci'
sh 'npm test -- --coverage --reporters=default --reporters=jest-junit'
}
post {
// always: the report matters most when the stage failed.
always {
junit testResults: 'reports/junit.xml', skipPublishingChecks: false
publishHTML(target: [
reportDir: 'coverage/lcov-report',
reportFiles: 'index.html',
reportName: 'Coverage'
])
}
}
}
stage('Integration') {
agent {
// Testcontainers needs a docker socket.
docker {
image 'node:22'
args '-v /var/run/docker.sock:/var/run/docker.sock --network host'
}
}
steps {
sh 'npm ci'
sh 'npm run test:integration'
}
post { always { junit 'reports/integration-junit.xml' } }
}
stage('E2E') {
agent {
docker {
// The official image has the browsers and their
// OS dependencies preinstalled.
image 'mcr.microsoft.com/playwright:v1.49.0-noble'
args '--ipc=host' // without this, Chromium crashes under load
}
}
steps {
sh 'npm ci'
sh 'npm run build'
sh 'npx playwright test --reporter=junit,html'
}
post {
always {
junit 'test-results/junit.xml'
archiveArtifacts artifacts: 'playwright-report/**', allowEmptyArchive: true
}
}
}
}
}
stage('Deploy') {
when { branch 'main' }
agent any
steps { sh './deploy.sh' }
}
}
post {
unstable {
// "unstable" means tests failed but the build itself worked —
// a distinction most platforms do not make.
slackSend channel: '#builds', color: 'warning',
message: "Tests failed: ${env.JOB_NAME} #${env.BUILD_NUMBER} (${env.BUILD_URL})"
}
failure {
slackSend channel: '#builds', color: 'danger',
message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
}
}
}Two container arguments matter more than they look:
--ipc=host for anything running Chromium. The default IPC namespace
gives 64MB of shared memory, and Chrome crashes intermittently under
parallel load without it. This is one of the most common causes of
"flaky browser tests in Jenkins" and it is not a
test problem at all.
Mounting the Docker socket for Testcontainers. Note the security implication: a container with the host's Docker socket can control the host. Use a dedicated agent for this, not a shared one.
#Dynamic parallel stages
Sharding, generated rather than copy-pasted:
stage('E2E sharded') {
steps {
script {
def shards = 4
def branches = [:]
(1..shards).each { i ->
branches["shard-${i}"] = {
node('linux && docker') {
checkout scm
docker.image('mcr.microsoft.com/playwright:v1.49.0-noble')
.inside('--ipc=host') {
sh 'npm ci'
sh "npx playwright test --shard=${i}/${shards} --reporter=blob"
stash name: "blob-${i}", includes: 'blob-report/**'
}
}
}
}
branches.failFast = false
parallel branches
}
}
}
stage('Merge report') {
steps {
script { (1..4).each { unstash "blob-${it}" } }
sh 'npx playwright merge-reports --reporter=html ./blob-report'
archiveArtifacts 'playwright-report/**'
}
}See Playwright sharding.
#Shared libraries
The single most important thing for keeping fifty repositories sane.
// vars/standardNodePipeline.groovy — in a separate, versioned repo
def call(Map config = [:]) {
pipeline {
agent none
stages {
stage('Checks') { /* ... */ }
stage('Tests') { /* ... */ }
}
}
}// Jenkinsfile in each repository
@Library('ci-standards@v3') _
standardNodePipeline(nodeVersion: '22', e2eShards: 4)Without this, every repository accumulates its own divergent copy of the same 200-line file and a platform change becomes fifty pull requests.
#The JUnit plugin
Jenkins' test reporting rests on JUnit XML, which every runner can emit:
junit(
testResults: '**/junit*.xml',
allowEmptyResults: false,
skipPublishingChecks: false,
// Flag a test that alternates between pass and fail on the same commit.
// Jenkins' detection is cruder than TeamCity's but it exists.
healthScaleFactor: 1.0
)Failing tests mark the build unstable (yellow) rather than failed (red) — a distinction Jenkins makes and most other platforms do not. It is useful: red means the pipeline itself broke, yellow means the code did.
#Where it fits
Choose Jenkins when you need self-hosted or air-gapped CI, complete control over agent hardware (GPUs, specific OS versions, physical devices), or integration with something only a Jenkins plugin supports.
Do not choose it for a small team on GitHub who would be served by GitHub Actions in a tenth of the setup time. The running cost of Jenkins is not the server; it is the plugin upgrades, the security advisories and the accumulated configuration nobody documented.
Common questions
- Should new projects use Jenkins?
- Usually not, unless you need what it uniquely offers — total control over agents, on-premise or air-gapped operation, or integration with something no hosted platform supports. Hosted CI is less work for most teams. Jenkins is the one you inherit, and it remains very capable once configured.
- Declarative or scripted pipelines?
- Declarative. It is more readable, it validates earlier, and it covers everything most pipelines need. Drop into a script block for the occasional piece of imperative logic rather than writing the whole file that way.
- How do I keep Jenkinsfiles from becoming unmaintainable?
- A shared library. Put the common pipeline shape in a versioned Groovy library and have each repository's Jenkinsfile be a handful of lines that calls it. Without this, fifty repositories means fifty divergent copies of the same 200-line file.
Runnable samples for this page
last test results ↗- YAML / Groovy / Kotlin
pipelines/ci-cd/jenkins
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- Testing in GitHub ActionsA complete pipeline — unit tests, integration with containers, sharded Playwright, coverage and artefacts — plus the caching and concurrency settings that make it fast.
- Testing in TeamCityBuild chains, real-time test reporting and the best flaky-test detection of any CI platform — configured as Kotlin DSL rather than clicked together.
- Terraform for Test InfrastructureProvisioning ephemeral test environments as code — per-branch stacks, workspaces, cost controls, and testing the Terraform itself.
- Test ReportingGetting results out of the runner and in front of people — JUnit XML, pull request annotations, per-test history, and the numbers worth publishing.
- Running Tests in ParallelHow parallelism turns hidden coupling into failures, the shared resources that clash, and how to make a suite genuinely safe to run concurrently.
- Testing in Bitbucket PipelinesThe simplest CI model of the five — steps, caches, services and parallel groups — with the hard limits you need to design around.