Running Selenium Tests in Jenkins

9 min read

Jenkins is the CI server enterprise QA teams use most. If your company has Java services in production, there's a 90% chance Jenkins is running them — and your Selenium suite will join the same pipeline. This lesson takes the Maven + TestNG + Page Object framework you've built across seven chapters and runs it from a real Jenkinsfile: declarative pipeline, parameterised builds, headless Chrome, TestNG result publishing, screenshot artefacts, and email-on-failure. By the end you'll have a Jenkinsfile you can drop into any Java/Selenium project and a clear understanding of every line.

What Jenkins gives you

Three things, in order of how often they matter:

  1. A reliable trigger. Code lands in main, Jenkins runs the suite. You don't run it; the system does, every time, including on weekends. The biggest single QA productivity win.
  2. A consistent environment. Tests run on the same agent OS, the same Java version, the same browser version — every time. No more "passes locally, fails on Carol's machine."
  3. Reports and artefacts. TestNG XML, Surefire HTML, screenshots, Allure reports — all archived per build, viewable in the browser, comparable across runs.

The cost: somebody has to install Jenkins, configure agents, install browsers, manage user accounts. For a team that already has Jenkins, the marginal cost of adding your suite is small. For a greenfield project without existing Jenkins, GitHub Actions (next lesson) is often the better start.

A complete Jenkinsfile

Jenkinsfile at the root of your repo:

pipeline {
    agent any
 
    tools {
        maven 'Maven-3.9'    // names match Jenkins → Manage → Tools
        jdk   'JDK-21'
    }
 
    parameters {
        choice(name: 'BROWSER',     choices: ['chrome', 'firefox', 'edge'])
        choice(name: 'ENVIRONMENT', choices: ['staging', 'production'])
        choice(name: 'SUITE',       choices: ['smoke.xml', 'regression.xml', 'cross-browser.xml'])
    }
 
    triggers {
        cron('H 6 * * *')   // nightly around 6am — H = hashed slot
    }
 
    options {
        timestamps()
        timeout(time: 30, unit: 'MINUTES')
        ansiColor('xterm')
    }
 
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
 
        stage('Build') {
            steps {
                sh 'mvn -B clean compile -DskipTests'
            }
        }
 
        stage('Test') {
            steps {
                sh """
                    mvn -B test \\
                        -DsuiteFile=${params.SUITE} \\
                        -Dbrowser=${params.BROWSER} \\
                        -Denv=${params.ENVIRONMENT} \\
                        -Dheadless=true
                """
            }
        }
    }
 
    post {
        always {
            junit testResults: 'target/surefire-reports/testng-results.xml',
                  allowEmptyResults: true
            archiveArtifacts artifacts: 'target/surefire-reports/**, target/screenshots/**',
                             allowEmptyArchive: true
        }
 
        failure {
            mail to:      'qa-team@example.com',
                 subject: "FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
                 body:    "See ${env.BUILD_URL}"
        }
    }
}

Read it as the pipeline architecture:

  • agent any — run on any free Jenkins agent. Real projects often pin to a label (agent { label 'linux-chrome' }) for agents pre-configured with browsers.
  • tools — names match the auto-installer entries you configure in Manage Jenkins → Tools. Jenkins fetches Maven 3.9 and JDK 21 if missing.
  • parameters — choices appear in the Jenkins UI. Run with Build with Parameters → pick browser, environment, suite. Same Jenkinsfile drives every variant.
  • triggers { cron(...) } — nightly schedule. The H in H 6 * * * is Jenkins-specific; it spreads jobs across the hour to avoid every job firing at exactly 06:00:00.
  • stages — checkout, build, test. mvn -B is "batch mode" (no interactive prompts, machine-friendly logs).
  • post — always archive results, mail on failure. The junit step parses TestNG XML so the build page shows pass/fail trends.

The pipeline visualised

Running headless on the agent

Jenkins agents are typically headless Linux servers — no display. The -Dheadless=true flag in the Maven invocation flips your BaseTest's headless toggle on. Combined with the Chrome flags you set in chapter 1 (--no-sandbox --disable-dev-shm-usage), Selenium runs cleanly under Linux containers.

If a test you genuinely need to run headed (visual debugging, video recording), use Xvfb — a virtual framebuffer:

stage('Test') {
    steps {
        sh 'xvfb-run -a mvn -B test -Dheadless=false'
    }
}

xvfb-run provides a fake X display so Chrome doesn't refuse to start. For 99% of CI cases though, --headless=new is faster and simpler.

Plugins worth installing

Jenkins ships a minimal core; almost every QA-flavoured feature is a plugin. The four most-used:

  • TestNG Results Plugin — parses testng-results.xml, shows trend graphs across builds. (The junit step in the pipeline above also handles TestNG XML, so this plugin is optional but nicer.)
  • Allure Jenkins Plugin — if you wired up Allure in chapter 5, this surfaces the rich HTML report inline on the build page. Generally the upgrade most teams make from raw Surefire HTML.
  • HTML Publisher Plugin — for any other HTML report (ExtentReports, custom dashboards). Wraps a folder of HTML and serves it from the build page.
  • Email Extension Plugin — replaces the basic mail step with templating, attachment support, and conditional sending.

Install via Manage Jenkins → Plugins → Available. The pipeline syntax for each is documented inline once installed.

Parameterised builds — letting QA pick from the UI

The parameters block above creates a "Build with Parameters" UI. A QA engineer picks "regression on Firefox against staging," clicks Build — they don't need to remember the Maven flags. The Jenkinsfile passes the choices to Maven via -D flags.

Common parameter shapes:

parameters {
    choice(name: 'SUITE',       choices: ['smoke.xml', 'regression.xml'])
    string(name: 'GIT_BRANCH',  defaultValue: 'main')
    booleanParam(name: 'HEADLESS', defaultValue: true)
    text(name: 'SLACK_MESSAGE', defaultValue: '')
}

Pair these with whatever your QA workflow needs — a "release smoke" job that defaults to the release branch and emails the release-manager on failure, an "ad-hoc cross-browser" job that lets you pick branch and browser combination.

Credentials — never in the Jenkinsfile

If your tests need an API key or password (test-environment credentials, BrowserStack key), use Jenkins Credentials, not a hardcoded value:

stage('Test') {
    steps {
        withCredentials([
            string(credentialsId: 'browserstack-user', variable: 'BS_USER'),
            string(credentialsId: 'browserstack-key',  variable: 'BS_KEY')
        ]) {
            sh 'mvn -B test -Dgrid.url=https://hub-cloud.browserstack.com/wd/hub'
        }
    }
}

withCredentials injects the secret as an environment variable scoped to that block. The variable doesn't appear in build logs (Jenkins masks it). Never commit secrets to the Jenkinsfile or pom.xml.

Cleanup — leak prevention on long-lived agents

Selenium leaks browser processes on test failure if driver.quit() doesn't fire. On a Jenkins agent that runs hundreds of builds per day, that's hundreds of zombie Chrome processes. Add a hard cleanup to post:

post {
    always {
        // Kill any stray browser processes
        sh '''
            pkill -f chromedriver || true
            pkill -f geckodriver  || true
            pkill -f chrome       || true
            pkill -f firefox      || true
        '''
    }
}

Brutal but effective. The || true keeps the step from failing the build when there are no processes to kill.

A real-world failure investigation flow

When a Jenkins build fails, the diagnosis flow is mechanical:

  1. Click the build → "Console Output" — the raw logs. Search for [ERROR] or FAILED.
  2. Click "Test Result" — the TestNG-parsed view. Click any failed test for stack trace + duration.
  3. Click "Build Artifacts" — open the screenshot for the failed test (which the listener from chapter 5 produced).
  4. Compare with previous green build — what changed? Often a flaky test or a real regression depending on the change.

Get a junior QA engineer comfortable with this loop in the first week and they'll close 80% of investigations themselves.

Comparison with GitHub Actions

The next lesson covers GitHub Actions in detail. The short version: Actions is simpler to set up (no server to maintain), tighter integration with GitHub PRs, free runners for public repos. Jenkins is more powerful for enterprise (RBAC, plugins, on-prem control) and is what 70%+ of large-enterprise QA teams use. Many shops have both — Actions for OSS projects, Jenkins for internal services.

The Selenium tool entry covers the test side; the TestNG cheat sheet and the CI/CD for Testers cheat sheet cover the Maven and pipeline flags this lesson uses.

⚠️ Common mistakes

  • Hardcoding credentials in the Jenkinsfile. A team-shared repo means everyone with read access sees the secret. Even if "it's just the staging password," credentials belong in Jenkins Credentials and reach the build via withCredentials. Never commit them.
  • No browser cleanup in post. A failing test that leaks a Chrome process is a one-off; a busy Jenkins agent with no cleanup accumulates dozens per day. Within a week the agent is OOM-ing or running into "address already in use" on the WebDriver port. The five-line pkill block prevents the entire class of failures.
  • Treating intermittent test failures as "Jenkins is being weird." Jenkins is an unbiased messenger — if a test is flaky, it shows up in the trend graph as red bars between green ones. Chase those down via the listener's screenshot and the retry-analyzer's logs. CI exposes flake faster than local runs do; that's a feature.

🎯 Practice task

Wire your suite into a real Jenkins pipeline. 45–60 minutes.

  1. Install Jenkins locally via Jenkins Dockerdocker run -p 8080:8080 jenkins/jenkins:lts. Open http://localhost:8080, complete the setup wizard.
  2. Configure tools: Manage Jenkins → Tools → add Maven 3.9 and JDK 21 with auto-installer. Save.
  3. Create a Pipeline job. Source: your Selenium repo (push a tiny project to a public GitHub repo if you don't have one). Script Path: Jenkinsfile. Add the Jenkinsfile from this lesson to your repo and push.
  4. Run "Build with Parameters." Pick chrome and smoke.xml. Watch the build run — checkout, build, test, post. Inspect the console output, the test result page, and the build artefacts.
  5. Force a failure. Make a test fail intentionally. Push. Run. Confirm the listener-generated screenshot is archived and visible from the build page. Confirm the email step would fire (it won't unless you've configured SMTP, which is fine to skip locally).
  6. Add cross-browser as separate stages. Refactor the Test stage into three parallel stages running Chrome, Firefox, and Edge concurrently:
    stage('Test') {
        parallel {
            stage('Chrome')  { steps { sh 'mvn test -Dbrowser=chrome -Dheadless=true'  } }
            stage('Firefox') { steps { sh 'mvn test -Dbrowser=firefox -Dheadless=true' } }
            stage('Edge')    { steps { sh 'mvn test -Dbrowser=edge -Dheadless=true'    } }
        }
    }
    The build page now shows three parallel columns. The total wall-clock time approaches the slowest browser, not the sum.
  7. Stretch — Selenium Grid in Docker on the agent. Add a dockerfile agent or services block to spin up a Grid in containers, then point your tests at -Dgrid.url=http://localhost:4444. The pipeline now runs against an ephemeral Grid that lives only for the duration of the build.

Next lesson: GitHub Actions. The simpler, faster-to-stand-up CI for projects that live on GitHub.

// tip to track lessons you complete and pick up where you left off across devices.