JaCoCo Setup for Android Multi Module Project

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! 👋

Published 19 Jan 2020

Technical Stuff. Rants
Esa Firman on Twitter