Skip to main content

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

Complete CI/CD pipeline configurations for JUnit 6 tests: GitHub Actions workflows with parallel stages, caching, and coverage gates; Jenkins declarative pipelines with JUnit result publishing; Testcontainers in CI environments; and Maven Surefire optimisation for CI.

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.

No Comments yet!

Leave a Reply

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