Running JUnit 6 Tests in CI/CD Pipelines (GitHub Actions, Jenkins)

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

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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.