Running tests locally is only half the story. The real value of automated tests comes when they run automatically on every push, pull request, and merge — in a CI/CD pipeline. This guide covers complete, production-ready pipeline configurations for both GitHub Actions and Jenkins, including caching, parallel stages, test result publishing, coverage gates, and Testcontainers support.
Why CI/CD for JUnit 6 Tests Matters
- Catch regressions before merge — no broken code reaches main
- Enforce coverage thresholds — automated gates prevent coverage drops
- Parallelise expensive tests — cut pipeline time from 20 minutes to 5
- Publish reports — test results and coverage visible in every PR
- Consistent environment — no more "works on my machine"
Part 1: GitHub Actions
Basic Pipeline: Build, Test, Report
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
# Cache Maven dependencies — speeds up subsequent runs significantly
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-
# Run unit tests on every push (fast)
- name: Run unit tests
run: mvn test -Dgroups=unit --batch-mode
# Run integration tests only on PRs to main (slower)
- name: Run integration tests
if: github.event_name == 'pull_request'
run: mvn verify -Dgroups=integration --batch-mode
# Publish JUnit XML results as a GitHub Check
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with:
files: target/surefire-reports/**/*.xml
# Upload JaCoCo HTML coverage report as artifact
- name: Upload coverage report
uses: actions/upload-artifact@v4
if: always()
with:
name: coverage-report
path: target/site/jacoco/
Advanced Pipeline: Parallel Matrix + Coverage Gate
# .github/workflows/full-ci.yml
name: Full CI with Parallel Stages
on:
pull_request:
branches: [ main ]
jobs:
# Stage 1: Unit tests (fast, runs first)
unit-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '17', distribution: 'temurin' }
- uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
- name: Unit tests with coverage
run: mvn test -Dgroups=unit jacoco:report --batch-mode
- name: Check coverage threshold (min 80%)
run: mvn jacoco:check --batch-mode
- uses: actions/upload-artifact@v4
with: { name: unit-coverage, path: target/site/jacoco/ }
# Stage 2: Integration tests (parallel with unit tests above)
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { java-version: '17', distribution: 'temurin' }
- uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
# Docker is pre-installed on ubuntu-latest — Testcontainers works out of the box
- name: Integration tests with Testcontainers
run: mvn verify -Dgroups=integration -DexcludedGroups=unit --batch-mode
- uses: EnricoMi/publish-unit-test-result-action@v2
if: always()
with: { files: target/failsafe-reports/**/*.xml }
# Stage 3: Deploy (only if both test stages pass)
deploy:
needs: [ unit-tests, integration-tests ] # waits for both parallel jobs
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to staging
run: echo "Deploying to staging..."
Part 2: Jenkins Pipeline
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent any
tools {
maven 'Maven-3.9'
jdk 'JDK-17'
}
environment {
// Testcontainers: disable ryuk container (often blocked in Jenkins)
TESTCONTAINERS_RYUK_DISABLED = 'true'
// Use local Docker socket
DOCKER_HOST = 'unix:///var/run/docker.sock'
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'https://github.com/your-org/your-repo.git'
}
}
stage('Unit Tests') {
steps {
sh 'mvn test -Dgroups=unit --batch-mode'
}
post {
always {
// Publish JUnit XML results in Jenkins UI
junit 'target/surefire-reports/**/*.xml'
}
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -Dgroups=integration --batch-mode'
}
post {
always {
junit 'target/failsafe-reports/**/*.xml'
}
}
}
stage('Coverage Report') {
steps {
sh 'mvn jacoco:report --batch-mode'
}
post {
always {
// Publish JaCoCo coverage in Jenkins UI
recordCoverage(tools: [[parser: 'JACOCO']])
// Archive HTML report
publishHTML(target: [
reportDir: 'target/site/jacoco',
reportFiles: 'index.html',
reportName: 'JaCoCo Coverage Report'
])
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
echo 'Deploying to production...'
}
}
}
post {
failure {
// Notify team on Slack or email when pipeline fails
echo 'Pipeline failed! Notifying team...'
}
success {
echo 'All tests passed ✔'
}
}
}
Testcontainers in CI: Key Configuration
# For GitHub Actions: Docker is pre-installed on ubuntu-latest runners
# No additional setup needed for Testcontainers
# For Jenkins: add these environment variables to your pipeline
TESTCONTAINERS_RYUK_DISABLED=true # disable cleanup container (often restricted)
DOCKER_HOST=unix:///var/run/docker.sock # use host Docker socket
# For GitLab CI: enable Docker-in-Docker service
# services:
# - docker:dind
# variables:
# DOCKER_HOST: tcp://docker:2376
# TESTCONTAINERS_HOST_OVERRIDE: docker
Maven Surefire: Optimising for CI
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<!-- Use fork count = number of CPU cores for parallel class execution -->
<forkCount>1C</forkCount> <!-- 1C = 1 JVM per CPU core -->
<reuseForks>true</reuseForks>
<!-- Fail fast in CI: stop after first failure to save time -->
<!-- <skipAfterFailureCount>1</skipAfterFailureCount> -->
<!-- XML report format required by most CI test result plugins -->
<reportFormat>xml</reportFormat>
<!-- Show test output on failure only (keep logs clean) -->
<useFile>true</useFile>
<trimStackTrace>false</trimStackTrace>
</configuration>
</plugin>
Frequently Asked Questions (FAQs)
Q1: How do I prevent a failed test from blocking the entire pipeline?
Use if: always() (GitHub Actions) or post { always { ... } } (Jenkins) to ensure test result publishing steps run even when tests fail. This gives you visibility into which tests failed rather than just a red pipeline with no details. For the overall pipeline to fail correctly, do NOT add continue-on-error: true to the test step itself — only to the reporting steps.
Q2: How do I cache Maven dependencies in GitHub Actions?
Use the actions/setup-java action with cache: 'maven' for automatic Maven caching, or use actions/cache with path: ~/.m2/repository for manual control. The cache key should hash your pom.xml files so the cache is invalidated whenever dependencies change. A warm Maven cache reduces pipeline time by 30–60 seconds on typical projects.
Q3: How do I run tests on multiple Java versions in CI?
Use a matrix strategy in GitHub Actions:
strategy:
matrix:
java: ['17', '21']
steps:
- uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
This runs the full test suite against both Java 17 and Java 21 in parallel, catching version-specific compatibility issues automatically.
Q4: How do I enforce a minimum code coverage threshold in CI?
Configure JaCoCo’s check goal in your pom.xml with a minimum coverage rule, then call mvn jacoco:check in your pipeline. If coverage drops below the threshold, the JaCoCo goal fails with a non-zero exit code, which fails the CI step. See Code Coverage with JaCoCo and JUnit 6 for the complete configuration.
Q5: Can I run a subset of tests in pull request pipelines and the full suite nightly?
Yes — this is the recommended pattern. In GitHub Actions, use on: pull_request triggers for fast unit tests (-Dgroups=unit) and on: schedule triggers for the full suite including slow integration and E2E tests. This gives developers fast feedback on PRs while ensuring the complete regression suite runs regularly. Use JUnit 6 Tags to control exactly which tests each pipeline stage executes.
See Also
- Tags and Test Suites in JUnit 6: Organizing Large Test Bases
- Code Coverage with JaCoCo and JUnit 6: Complete Setup
- Test Reporting in JUnit 6: Allure vs Surefire vs Custom Reports
- JUnit 6 with Testcontainers: Real Database Integration Testing
- Parallel Test Execution in JUnit 6: Configuration and Pitfalls
Conclusion
A well-configured CI/CD pipeline transforms your test suite from a local safety net into a team-wide quality gate. Tag your tests to control which stages run when, cache dependencies to keep pipelines fast, configure Testcontainers for Docker-based CI environments, and always publish test results and coverage reports as first-class pipeline artifacts. Apply these patterns and every pull request becomes a fully validated, documented change.
Next: Code Coverage with JaCoCo and JUnit 6 — measure what your tests actually cover and enforce thresholds that prevent coverage regression.