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:
- Triggers —
push,pull_request, and a nightlyschedule. Push for staging confidence, PR for change confidence, nightly for catching environmental drift the daytime suite missed. actions/setup-javawithcache: 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. TheConfigclass from Chapter 6 already readsSystem.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.comThe 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-historyThe 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) andgitleaksare 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 testwithout-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-minuteson 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).
- Create the workflow. Add
.github/workflows/api-tests.ymlmatching the lesson's shape. Set Java 21, Maven cache enabled,mvn -B clean test. - Add secrets. In your GitHub repo settings → Secrets, add
STAGING_API_URL,ADMIN_EMAIL,ADMIN_PASSWORD. Reference them in the workflowenv. Push the branch; watch the workflow run. - 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.
- Trigger on PR. Open a PR. Confirm the workflow runs as a check on the PR. Confirm the failing run blocks the merge button.
- 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. - Allure. Add
allure-rest-assuredto the pom. WireRestAssured.filters(new AllureRestAssured())inBaseApiTest. Add the Allure-report-action and gh-pages publishing step. Visit the published URL. - Smoke vs regression. Create
smoke.xmlwith<include name="smoke"/>. Tag five tests@Test(groups = "smoke"). Run the workflow with-DsuiteXmlFile=src/test/resources/smoke.xmlfor PRs. Have the nightly run useregression.xml(or no filter). - Stretch: add a Slack notification on failure. Use
rtCamp/action-slack-notifywithif: 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.