Running Rest Assured Tests in CI/CD Pipelines

8 min read

A test suite that runs only on a developer's laptop has zero value to the rest of the team. The whole point of automation is catching regressions before humans see them — and that means running the suite on every push, every PR, every nightly schedule. This lesson is the small set of CI patterns every Rest Assured suite eventually grows: a GitHub Actions workflow, environment-aware configuration, secret management, test artefacts uploaded for debugging, and reports humans can scan in 30 seconds. The framework you've built so far is already CI-ready; this lesson is the wiring that turns a mvn test invocation into a regression net.

A first GitHub Actions workflow

.github/workflows/api-tests.yml:

name: API Tests
on:
  push:
    branches: [main, develop]
  pull_request:
  schedule:
    - cron: "0 2 * * *"   # nightly at 02:00 UTC
 
jobs:
  api-tests:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-java@v4
        with:
          java-version: "21"
          distribution: "temurin"
          cache: maven
 
      - name: Run API tests
        run: mvn -B clean test -DsuiteXmlFile=src/test/resources/testng.xml
        env:
          API_BASE_URI:    ${{ secrets.STAGING_API_URL }}
          ADMIN_EMAIL:     ${{ secrets.ADMIN_EMAIL }}
          ADMIN_PASSWORD:  ${{ secrets.ADMIN_PASSWORD }}
 
      - name: Upload Surefire reports
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: surefire-reports
          path: target/surefire-reports/
 
      - name: Upload Allure results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: allure-results
          path: target/allure-results/

Five things working together:

  • Triggerspush, pull_request, and a nightly schedule. Push for staging confidence, PR for change confidence, nightly for catching environmental drift the daytime suite missed.
  • actions/setup-java with cache: maven — first run is slow (downloads dependencies), subsequent runs are fast (Maven local repo cached).
  • Secrets via ${{ secrets.* }} — defined once in the GitHub repo settings, never in the YAML or source. The Config class from Chapter 6 already reads System.getenv("..."), so this just works.
  • if: always() on the artefact uploads — failed runs need their reports more than passing runs do.
  • timeout-minutes — guard against a hung suite eating the whole day's runner budget.

Jenkins, for the on-prem case

The same shape, declarative-pipeline syntax:

pipeline {
    agent any
    tools {
        maven 'Maven-3.9'
        jdk   'JDK-21'
    }
    environment {
        API_BASE_URI = "${env.STAGING_URL ?: 'https://staging.api.example.com'}"
    }
    options { timeout(time: 20, unit: 'MINUTES') }
 
    stages {
        stage('Run API tests') {
            steps {
                withCredentials([
                    string(credentialsId: 'admin-password', variable: 'ADMIN_PASSWORD')
                ]) {
                    sh 'mvn -B clean test -DsuiteXmlFile=src/test/resources/testng.xml'
                }
            }
        }
    }
 
    post {
        always {
            junit 'target/surefire-reports/*.xml'
            archiveArtifacts artifacts: 'target/surefire-reports/**', allowEmptyArchive: true
            archiveArtifacts artifacts: 'target/allure-results/**', allowEmptyArchive: true
        }
    }
}

withCredentials is Jenkins's idiom for secret injection — same role as GitHub's secrets context. The post { always {} } block uploads artefacts whether the suite passes or fails. The Jenkins JUnit plugin (junit '...') parses the Surefire XMLs into a clickable test report, identical in role to GitHub's Surefire artefact + a viewer.

Environment-aware configuration

The Config class from Chapter 6 reads system properties, falls back to environment variables, falls back to defaults. CI sets the env vars; local dev uses defaults or -D... flags. The same JAR runs in every environment without recompilation:

# Local default (staging)
mvn test
 
# Local override to a feature branch deploy
mvn test -DbaseUri=https://feature-payments.preview.example.com
 
# CI staging (env vars set by GitHub Actions)
API_BASE_URI=https://staging.api.example.com \
  ADMIN_PASSWORD=$STAGING_PW mvn test
 
# Production smoke (read-only tests, separate suite)
mvn test -DsuiteXmlFile=smoke-prod.xml \
  -DbaseUri=https://api.example.com

The discipline: the suite never knows where it's running — it just reads its environment and does what it's told. That's what makes CI integration trivial.

TestNG suite XML for selective runs

A testng.xml file lets the team curate "what runs on what trigger":

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="API Test Suite" parallel="methods" thread-count="4">
  <test name="Smoke">
    <groups>
      <run>
        <include name="smoke"/>
      </run>
    </groups>
    <packages>
      <package name="com.mycompany.apitests.tests.*"/>
    </packages>
  </test>
</suite>

Tests tagged @Test(groups = "smoke") run on PR (-DsuiteXmlFile=smoke.xml); the full regression runs nightly. A separate regression.xml with no group filter pulls in everything. The full-vs-smoke split is what keeps PR feedback fast and nightly runs comprehensive.

Reports that humans actually open

Surefire's XML reports are machine-readable but ugly. Two upgrades:

Allure. Adds allure-rest-assured as a filter (covered in Chapter 6) — every Rest Assured request is attached to the test step. The report is a clickable HTML site; failures take 10 seconds to diagnose.

<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-rest-assured</artifactId>
    <version>2.27.0</version>
</dependency>
- name: Generate Allure report
  if: always()
  uses: simple-elf/allure-report-action@v1.7
  with:
    allure_results: target/allure-results
    allure_history: allure-history
 
- name: Publish Allure to GitHub Pages
  if: always() && github.ref == 'refs/heads/main'
  uses: peaceiris/actions-gh-pages@v3
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    publish_dir: allure-history

The peaceiris/actions-gh-pages step publishes the report to a GitHub Pages site — a permanent URL the team can bookmark. Test runs become discoverable, not buried in CI logs.

ExtentReports. Lighter-weight alternative; one HTML file under target/, uploaded as an artefact. Use Allure for richer reporting, ExtentReports if Allure feels heavy.

The CI loop, end to end

Every link in the chain is a few lines of YAML. Together they convert "tests exist" into "tests gate every change."

Smoke vs regression — the schedule that scales

The most useful pattern for a real team:

  • PR runs: smoke suite only — happy paths, the most-likely-to-break tests, ~5 minutes. Fast feedback for the PR author.
  • Main-branch push: full regression suite — every test, ~20 minutes. Catches cross-feature interactions.
  • Nightly: the full suite plus slow tests (long timeouts, large dataset uploads, parallel-stress tests) — ~60 minutes. Catches drift over time, infrastructure flake, scheduled-job interactions.
  • Hourly (production): read-only smoke suite against prod — 5 tests, 30 seconds. Continuous health check.

Each schedule has a different testng.xml (or a different groups filter). The Config class points each at the right base URI via env vars. The framework is the same; the trigger is what differs.

Failure notifications

The CI run is useless if nobody notices it failed. A 10-line Slack notification:

- name: Notify Slack on failure
  if: failure()
  uses: rtCamp/action-slack-notify@v2
  env:
    SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
    SLACK_COLOR: danger
    SLACK_TITLE: "API tests failed on ${{ github.ref_name }}"
    SLACK_MESSAGE: "Run ${{ github.run_id }} — ${{ github.event.head_commit.message }}"

if: failure() runs only on red builds; the channel only sees noise when there's something to act on. Add a if: success() && github.event_name == 'schedule' step to send a green ping for nightly runs (a missing-cron-job alarm is a real failure mode).

Secrets — the rules

The non-negotiables, repeated because every team eventually breaks them:

  • Never in git — not in application.yml, not in commented-out code, not in fixture files. git secrets (the AWS tool) and gitleaks are pre-commit hooks that catch leaks; install one.
  • Never in CI logs — the LogConfig.blacklistHeader("Authorization") from Chapter 6 keeps tokens out. Verify by deliberately failing one test and reading the log artefact.
  • Rotated regularly — CI secrets that haven't changed in two years are someone's exit-interview risk. A quarterly rotation cadence is the floor.
  • Scoped narrowly — the test-suite credentials should have only the access tests need. A read-only smoke against prod uses a read-only token; even leaked, the blast radius is small.

⚠️ Common mistakes

  • mvn test without -B. The -B (batch mode) flag silences interactive prompts and reduces log noise — without it, CI logs are a wall of progress spinners. Always use it on automated runs.
  • No timeout on the job. A hung test (deadlock, infinite retry, network stall) holds the runner indefinitely. Set timeout-minutes on every CI job — 2× your expected run time is the right ballpark.
  • Reports that nobody reads. A CI that generates Allure but never publishes the report (or publishes it but nobody knows where) is a tree falling in an empty forest. Wire the publishing step and tell the team where the report lives.

🎯 Practice task

Wire your test suite into GitHub Actions. 30–60 minutes (most of which is iterating on the YAML).

  1. Create the workflow. Add .github/workflows/api-tests.yml matching the lesson's shape. Set Java 21, Maven cache enabled, mvn -B clean test.
  2. Add secrets. In your GitHub repo settings → Secrets, add STAGING_API_URL, ADMIN_EMAIL, ADMIN_PASSWORD. Reference them in the workflow env. Push the branch; watch the workflow run.
  3. First failure to be helpful. Force a deliberate failure (change one assertion). Push. Confirm the workflow fails, the artefact uploads, and you can download the Surefire reports from the run summary. Restore.
  4. Trigger on PR. Open a PR. Confirm the workflow runs as a check on the PR. Confirm the failing run blocks the merge button.
  5. Schedule. Add a cron: "0 2 * * *" schedule. Wait for tomorrow morning's run (or trigger manually via "workflow_dispatch") and confirm it runs against staging.
  6. Allure. Add allure-rest-assured to the pom. Wire RestAssured.filters(new AllureRestAssured()) in BaseApiTest. Add the Allure-report-action and gh-pages publishing step. Visit the published URL.
  7. Smoke vs regression. Create smoke.xml with <include name="smoke"/>. Tag five tests @Test(groups = "smoke"). Run the workflow with -DsuiteXmlFile=src/test/resources/smoke.xml for PRs. Have the nightly run use regression.xml (or no filter).
  8. Stretch: add a Slack notification on failure. Use rtCamp/action-slack-notify with if: failure(). Force a failure to verify the message lands. Bonus: include the failing test name in the message.

That's data-driven testing and CI/CD covered. Chapter 8 is the capstone — a complete BookVault library API framework from project scaffold through CI, applying every chapter together.

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