Skip to main content

RUNNING TESTS

CI/CD

Run Karate tests in CI, publish the HTML reports as a live artifact your team can link to, and guard against leaking credentials into those reports. This page is organised around a single reference implementation — karate-todo — so every pattern shown here is green on main and you can open the resulting reports right now.

See it live

Reports from the reference implementation's latest green build:

Full workflow source: .github/workflows/cicd.yml.

Workflow shape​

The reference workflow stages jobs linearly so a failure short-circuits the pipeline:

tests ──▶ gatling ──▶ secret-scan ──▶ publish (main only)
JobWhat it does
tests./mvnw verify -Pui — one hybrid suite (API + UI) via Testcontainers + headless Chrome
gatlingStarts the app in the background, runs a Gatling smoke, uploads the HTML report
secret-scanGreps the Karate and Gatling report artifacts for common token / private-key patterns
publishOn main only: deploys the assembled reports to gh-pages

needs: wires the dependencies so the downstream jobs only run if everything before them passed, and artifacts flow from job to job.

Running tests in GitHub Actions​

The tests job runs a consolidated Karate suite that drives both the API and a containerised Chrome against the same in-process app:

.github/workflows/cicd.yml
tests:
name: API + UI tests (Testcontainers)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Run api + ui suite (chromedp/headless-shell via Testcontainers)
run: ./mvnw -B verify -Pui
- name: Upload Karate report
if: always()
uses: actions/upload-artifact@v4
with:
name: karate-report
path: target/karate-reports
retention-days: 14
Node.js 24 opt-in

Set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" at the workflow level to pre-empt GitHub's 2026-06-02 Node 20 deprecation before it starts forcing the switch.

Hybrid API + UI in one suite​

The reference UiTest runs both API and UI features from a single run, so the published report covers the full surface:

src/test/java/app/ui/UiTest.java
@Test
void testAll() {
ContainerDriverProvider provider = new ContainerDriverProvider(chrome);
SuiteResult result = Runner.path("classpath:app/api", "classpath:app/ui")
.tags("~@external", "~@todo")
.systemProperty("serverUrl", chrome.getHostAccessUrl(PORT))
.systemProperty("apiUrl", "http://localhost:" + PORT)
.driverProvider(provider)
.parallel(1);
assertEquals(0, result.getScenarioFailedCount(), String.join("\n", result.getErrors()));
}

Two URLs, one app:

  • serverUrl — what the browser navigates to. Inside the Chrome container this is host.docker.internal:<port> (Docker Desktop on macOS/Windows) or the host-gateway mapping on native Linux.
  • apiUrl — what Karate's HTTP client (running on the host) uses. Plain localhost:<port>.

karate-config.js picks these up:

src/test/java/karate-config.js
function fn() {
var serverUrl = karate.properties['serverUrl'] || 'http://localhost:8080';
var apiUrl = karate.properties['apiUrl'] || serverUrl;
karate.configure('driver', { type: 'chrome' });
return { serverUrl: serverUrl, apiUrl: apiUrl };
}

See UI Testing — Testcontainers for the ChromeContainer + ContainerDriverProvider pattern behind this.

Publishing HTML reports to GitHub Pages​

Every green push to main deploys target/karate-reports/ and the Gatling output to a gh-pages branch, giving you a stable URL to paste into release notes, Slack, or trainings.

.github/workflows/cicd.yml
publish:
name: Publish reports to gh-pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: secret-scan
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: karate-report
path: public/latest/karate
- uses: actions/download-artifact@v4
with:
name: gatling-report
path: public/latest/gatling-raw
- name: Flatten Gatling output
run: |
# Gatling writes into `gatling/todosimulation-<timestamp>/` — move the
# contents up one level so /latest/gatling/ serves index.html directly.
sim=$(ls -d public/latest/gatling-raw/todosimulation-* | head -1)
mv "$sim" public/latest/gatling
rm -rf public/latest/gatling-raw
- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: public
keep_files: false
Enabling Pages

The first deploy creates the gh-pages branch. Enable Pages via Settings → Pages → Source = Deploy from a branch → gh-pages / root, or from the command line:

gh api repos/<owner>/<repo>/pages -X POST --input - <<'EOF'
{"build_type": "legacy", "source": {"branch": "gh-pages", "path": "/"}}
EOF

Scanning reports for leaked secrets​

HTML reports are great for transparency — and dangerous if a test accidentally logs an Authorization header or a token. A short grep step at the end of the pipeline catches the common patterns:

.github/workflows/cicd.yml
secret-scan:
name: Secret-leak scan
needs: gatling
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: karate-report
path: reports/karate
- uses: actions/download-artifact@v4
with:
name: gatling-report
path: reports/gatling
- name: Grep reports for known secret patterns
run: |
set +e
PATTERNS='Bearer [A-Za-z0-9_.\-]{20,}|gh[pousr]_[A-Za-z0-9]{30,}|sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----'
MATCHES=$(grep -REn "$PATTERNS" reports/ || true)
if [ -n "$MATCHES" ]; then
echo "::error::secrets found in published reports:"
echo "$MATCHES"
exit 1
fi
echo "no secret patterns matched"

The patterns cover Bearer tokens of reasonable length, GitHub PATs / OAuth / server-to-server tokens, OpenAI-style keys, AWS access key IDs, and PEM-encoded private keys. Extend the regex for organisation-specific formats.

Defence in depth

This is a last-line check on what actually reaches the report. Complement it with a pre-commit gitleaks or trufflehog scan so secrets never land in the feature files in the first place.

Gatling smoke with a background app​

Gatling needs the app up on a known port. The simplest pattern: start the app in the background, poll the port, run Gatling, kill the app.

.github/workflows/cicd.yml
gatling:
name: Gatling smoke
needs: tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven
- name: Start app in background
run: |
mkdir -p target
nohup ./mvnw -B test -Dtest=LocalRunner > target/local-runner.log 2>&1 &
echo $! > target/local-runner.pid
for i in {1..60}; do
if nc -z localhost 8080; then break; fi
sleep 1
done
nc -z localhost 8080 || (echo "app did not start"; cat target/local-runner.log; exit 1)
- name: Run Gatling simulation
run: ./mvnw -B test -P gatling
- name: Stop app
if: always()
run: kill "$(cat target/local-runner.pid)" || true
Why not a service container?

GitHub Actions services: want a published image. Starting the app in the same workspace keeps the workflow self-contained (no registry, no extra Dockerfile) and matches what trainers do in two terminals locally.

Filtering tests per CI stage​

Tag filtering is how you keep CI jobs focused. In the reference implementation:

TagUsed for
@externalFeatures that hit real external hosts (httpbin, jsonplaceholder, google) — excluded from CI via ~@external
@todoFeatures gated on an upstream fix — excluded from CI via ~@todo
@smokeFast critical path — can be used to gate earlier jobs

The tests job runs everything non-@external and non-@todo. An earlier "smoke" job could run .tags("@smoke") only. See Tags for the full filtering grammar.

Codespaces​

The same commands work inside a GitHub Codespace if the devcontainer ships with Docker-in-Docker and Java 21:

.devcontainer/devcontainer.json
{
"image": "mcr.microsoft.com/devcontainers/java:1-21-bullseye",
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "none",
"installMaven": "true"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
"forwardPorts": [8080, 5500],
"customizations": {
"vscode": {
"extensions": [
"redhat.java",
"karatelabs.karate",
"ms-vscode.live-server"
]
}
}
}

After ./mvnw verify -Pui, right-click any generated report HTML and Open with Live Server — port 5500 is forwarded, so the rendered report opens in a browser tab exactly as it would locally.

Jenkins​

For Jenkins pipelines, archive the same artefacts and publish via the HTML Publisher plugin:

Jenkinsfile
pipeline {
agent any
stages {
stage('Test') {
steps {
sh './mvnw -B verify -Pui'
}
post {
always {
publishHTML([
reportDir: 'target/karate-reports',
reportFiles: 'karate-summary.html',
reportName: 'Karate Report',
keepAll: true
])
junit 'target/surefire-reports/*.xml'
}
}
}
}
}

Next steps​