Published on

JaCoCo Setup for Android Multi Module Project

Authors

In a multi-module project structure, we can easily limit what to test and know exactly what is the coverage of that certain module. But the configuration could be verbose if we define it in every build.gradle

There is another use case when we want to combine/aggregate all modules coverage in one report. Unfortunately, this is also not covered automatically by JaCoCo.

So how we can achieve easy and centralized JaCoCo config for our multi-module projects?

Apply JaCoCo

The first thing to do is to create a separate gradle file where we write our configuration. Let's name it jacoco.gradle and put it on gradle directory

## This is our project structure.
## We use standard Android project structure.

├── app
│  └── src
├── gradle
│  ├── jacoco.gradle
│  └── wrapper

In this file let's apply the JaCoCo gradle plugin for our modules

// In here we can filter out what modules that we want to cover
def coveredProject = subprojects

// configure() method takes a list as an argument and applies the configuration to the projects in this list.
configure(coveredProject) { prj ->

    // Here we apply jacoco plugin to every project
    apply plugin: 'jacoco'

    // Set Jacoco version
    jacoco {
        toolVersion = "0.8.5"
    }

    // Here we create the task to generate Jacoco report
    // It depends to unit test task we don't have to manually running unit test before the task
    task jacocoReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {

        // Define what type of report we should generate
        // If we don't want to process the data further, html should be enough
        reports {
            csv.enabled = false
            xml.enabled = false
            html.enabled = true
        }

        // Setup the .class, source, and execution directories
        final fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', 'android/**/*.*']

        // Include this if you use Kotlin
        final kotlinTree = fileTree(dir: "${prj.buildDir}/tmp/kotlin-classes/debug", excludes: fileFilter)
        final javacTree = fileTree(dir: "${prj.buildDir}/intermediates/javac/debug", excludes: fileFilter)
        final mainSrc = "${prj.projectDir}/src/main/java"

        sourceDirectories.setFrom files([mainSrc])
        classDirectories.setFrom files([kotlinTree, javacTree])
        executionData.setFrom fileTree(dir: prj.buildDir, includes: [
            'jacoco/testDebugUnitTest.exec', 'outputs/code-coverage/connected/*coverage.ec'
        ])
    }
}

Now we're ready, to run the unit test and generate the code coverage you can use

$ ./gradlew <module_name>:jacocoReport

Aggregate The Coverage

Now, to create the aggregated data what you have to do is combine the jacocoReport task Let's add some more configuration to our jacoco.gradle


apply plugin: 'jacoco'

task jacocoFullReport(type: JacocoReport, group: 'Coverage reports') {
    def projects = coveredProject

    // Here we depend on the jacocoReport task that we created before
    dependsOn(projects.jacocoReport)

    final source = files(projects.jacocoReport.sourceDirectories)

    additionalSourceDirs.setFrom source
    sourceDirectories.setFrom source

    classDirectories.setFrom files(projects.jacocoReport.classDirectories)
    executionData.setFrom files(projects.jacocoReport.executionData)

    reports {
        html {
            enabled true
            destination file('build/reports/jacoco/html')
        }
        csv {
            enabled true
            destination file('build/reports/jacoco/jacocoFullReport.csv')
        }
    }

    doFirst {
        //noinspection GroovyAssignabilityCheck
        executionData.setFrom files(executionData.findAll { it.exists() })
    }
}

That's it! To create the aggregated report you can run this in your terminal

$ ./gradlew jacocoFullReport

You can find the full example in here

Until next time! 👋