Code Coverage on Android with JaCoCo
Many articles have been written on code coverage since the feature was introduced to Android in vesion 0.10.0 of the Gradle plugin — I have no illusions about that. However, what frustrates me is having to look through several of them and even some Gradle documentation before you can get a full working solution. So here goes another article in an attempt to remedy that and save you time.
The Problem
Given an Android project with unit tests, we want to generate a code coverage report for the executed tests. The solution must support different build types and product flavours.
The Solution
There are different parts to the solution, so let’s look at it in steps.
Enable Code Coverage
You need to enable the code coverage support for the build type that you will be testing with. Your build.gradle should include the following:
android {
...
buildTypes {
debug {
testCoverageEnabled = true
}
...
}
...
}
Set up JaCoCo
Although everything in this section can be included in your build.gradle, the open wiring will make your build script unreadable, so I suggest evicting it all into a separate build script and then importing it.
We start the JaCoCo setup by creating a file jacoco.gradle in the project root. You can create it anywhere you like, but having it in the root means that all subprojects can easily reference it.
The easy part is importing JaCoCo:
apply plugin: 'jacoco' jacoco {
toolVersion = "0.7.5.201505241946"
}
Notice that you don’t need to define any dependencies to apply the “jacoco” plugin — this is all handled by the Android plugin.
To check the latest version, search for org.jacoco:org.jacoco.core on jCenter, but be careful upgrading — the latest version may not be compatible yet, resulting in quirks like empty coverage reports.
Next step is generating the Gradle tasks for all product flavours and build types (you should really be testing debug, but it could be handy for any special types of debug that you may have):
def buildTypes = android.buildTypes.collect { type -> type.name }
def productFlavors = android.productFlavors.collect { flavor -> flavor.name }
Note that collect in Groovy takes a list, applies a function to its elements and outputs the resulting list. In this case it takes lists of build type and product flavour objects and turns them into lists containing their names.
To cater for projects with no product flavours, we add an empty one:
if (!productFlavors) productFlavors.add('')
Now we can iterate over them in what is essentially a nested loop in Groovy:
productFlavors.each { productFlavorName ->
buildTypes.each { buildTypeName ->
...
}
}
The most important part is what goes inside, so let’s go through it in more detail.
First, we prepare the task names with proper capitalisation:
sourceName
— build source name, e.g. blueDebugsourcePath
— build source path, e.g. blue/debugtestTaskName
— test task that the coverage task depends on, e.g. testBlueDebug
Here is how we define them:
def sourceName, sourcePath
if (!productFlavorName) {
sourceName = sourcePath = "${buildTypeName}"
} else {
sourceName = "${productFlavorName}${buildTypeName.capitalize()}"
sourcePath = "${productFlavorName}/${buildTypeName}"
}
def testTaskName = "test${sourceName.capitalize()}UnitTest"
Now the actual task:
task "${testTaskName}Coverage" (type:JacocoReport, dependsOn: "$testTaskName") {
group = "Reporting"
description = "Generate Jacoco coverage reports on the ${sourceName.capitalize()} build." classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes/${sourcePath}",
excludes: ['**/R.class', '**/R$*.class', '**/*$ViewInjector*.*', '**/BuildConfig.*', '**/Manifest*.*'] ) def coverageSourceDirs = [
"src/main/java",
"src/$productFlavorName/java",
"src/$buildTypeName/java"
]
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec") reports {
xml.enabled = true
html.enabled = true
}}
You might have seen similar code in other articles about JaCoCo, so hopefully most of it is self-explanatory.
The noteworthy parts are:
classDirectories
— you can list patterns to exclude from the coverage report in “excludes”; these could be generated code (R class, dependency injectors etc) or anything else that you may want to ignorereports
— enable HTML and/or XML reports, depending on whether you need them for displaying or parsing, respectively
That’s it for jacoco.gradle, so here is the full file:
apply plugin: 'jacoco' jacoco {
toolVersion = "0.7.5.201505241946"
} project.afterEvaluate {
// Grab all build types and product flavors
def buildTypes = android.buildTypes.collect { type ->
type.name
}
def productFlavors = android.productFlavors.collect { flavor ->
flavor.name
} // When no product flavors defined, use empty
if (!productFlavors) productFlavors.add('') productFlavors.each { productFlavorName ->
buildTypes.each { buildTypeName ->
def sourceName, sourcePath
if (!productFlavorName) {
sourceName = sourcePath = "${buildTypeName}"
} else {
sourceName = "${productFlavorName}${buildTypeName.capitalize()}"
sourcePath = "${productFlavorName}/${buildTypeName}"
}
def testTaskName = "test${sourceName.capitalize()}UnitTest" // Create coverage task of form 'testFlavorTypeCoverage' depending on 'testFlavorTypeUnitTest'
task "${testTaskName}Coverage" (type:JacocoReport, dependsOn: "$testTaskName") {
group = "Reporting"
description = "Generate Jacoco coverage reports on the ${sourceName.capitalize()} build." classDirectories = fileTree(
dir: "${project.buildDir}/intermediates/classes/${sourcePath}",
excludes: [
'**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
'**/*$ViewBinder*.*',
'**/BuildConfig.*',
'**/Manifest*.*'
]
) def coverageSourceDirs = [
"src/main/java",
"src/$productFlavorName/java",
"src/$buildTypeName/java"
]
additionalSourceDirs = files(coverageSourceDirs)
sourceDirectories = files(coverageSourceDirs)
executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec") reports {
xml.enabled = true
html.enabled = true
}
}
}
}
}
Finally, you need to import this build script in your app script like so:
apply from: '../jacoco.gradle'
(Note: This assumes that jacoco.gradle is in the project root, as explained before)
That’s it! You can verify the tasks are being generated by running gradle tasks
and seeing something similar to the below in the “Reporting” section:
Reporting tasks
---------------
testBlueDebugUnitTestCoverage - Generate Jacoco coverage reports on the BlueDebug build.
testBlueReleaseUnitTestCoverage - Generate Jacoco coverage reports on the BlueRelease build.
testRedDebugUnitTestCoverage - Generate Jacoco coverage reports on the RedDebug build.
testRedReleaseUnitTestCoverage - Generate Jacoco coverage reports on the RedRelease build.
To generate a report, run gradle testBlueDebugUnitTestCoverage
and you will see them in “build/reports/jacoco/testBlueDebugUnitTestCoverage/”.
Source
- JaCoCo example (GitHub)
Originally published at blog.gouline.net on June 23, 2015.