Cross-module code coverage with jacoco and gradle multi-module project

Speaking about SonarQube: you can get aggregated report by using same location for jacoco.exec across all modules. Make sure that file is removed before build and appended in all modules.

Speaking solely about Gradle: have a look on

  • https://discuss.gradle.org/t/merge-jacoco-coverage-reports-for-multiproject-setups/12100
  • https://gist.github.com/aalmiray/e6f54aa4b3803be0bcac

subprojects {
    apply(plugin: 'org.jetbrains.kotlin.jvm')

    repositories {
        jcenter()
        mavenCentral()
   }
}

task codeCoverageReport(type: JacocoReport) {

    // Gather execution data from all subprojects
    executionData fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec")

    // Add all relevant sourcesets from the subprojects
    subprojects.each {
        sourceSets it.sourceSets.main
    }

    reports {
        xml.enabled true
        html.enabled true
        csv.enabled false
    }
}

// always run the tests before generating the report
codeCoverageReport.dependsOn {
    subprojects*.test
}

sonarqube {
    properties {
        property "sonar.projectKey", "your_project_key"
        property "sonar.verbose", true
        property "sonar.projectName", "Your project name"
        property "sonar.coverage.jacoco.xmlReportPaths", "${rootDir}/build/reports/jacoco/codeCoverageReport/codeCoverageReport.xml"
    }
}

Command to run test with coverage:

./gradlew codeCoverageReport
./gradlew sonarqube -x test (test is excluded since already run and sonarqube by default executes test)

Two things to be noted that made it work:

  1. To make available sourcesets of all modules, looping over subprojects and accumulating sourcesets worked. subprojects.sourceSets.main.allSource.srcDirs did not work.
  2. sonar.jacoco.reportPaths is deprecated. We need to use sonar.coverage.jacoco.xmlReportPaths. Check the documentation here


As noted in the comments, you must first merge the Jacoco execution data and then tell sonarqube to use that instead of the individual exec files generated by each submodule.

I'm adding an example here since the links provided in the accepted answer are a little bit misleading. Most of them provide you with different workarounds to merge Jacoco reports, not to merge the execution data, which is what you want.

Here's how it would look like:

def allTestCoverageFile = "$buildDir/jacoco/allTestCoverage.exec"

sonarqube {
    properties {
        property "sonar.projectKey", "your.org:YourProject"
        property "sonar.projectName", "YourProject"
        property "sonar.jacoco.reportPaths", allTestCoverageFile
    }
}

task jacocoMergeTest(type: JacocoMerge) {
    destinationFile = file(allTestCoverageFile)
    executionData = project.fileTree(dir: '.', include:'**/build/jacoco/test.exec')
}

task jacocoMerge(dependsOn: ['jacocoMergeTest']) {
    // used to run the other merge tasks
}

subprojects {
    sonarqube {
        properties {
            property "sonar.jacoco.reportPaths", allTestCoverageFile
        }
    }
}

In a nutshell:

  • First, we define a global coverage file output for our test reports (allTestCoverageFile).
  • Then we need to tell Sonarqube to use that file (using sonar.jacoco.reportPaths). But notice we also have to do it in the subprojects closure. This is extremely important. Don’t miss it.
  • Finally, we create a custom task that extends from JacocoMerge (an incubating class from the Jacoco plugin), that merges all the test coverage reports from all projects (executionData) into our allTestCoverageFile.

If you are using a version of SonarQube prior to 6.2 please use sonar.jacoco.reportPath property