Coverage Code with Gradle Convention Plugins and Jacoco for Android

Fabrice PITOISET
Publicis Sapient France
6 min readJun 3, 2024

The purpose of this article is to create a Gradle Convention Plugin for code coverage on Android application projects using Jacoco.

But first let’s remember what a Gradle Convention Plugin and Jacoco are.

Gradle Convention Plugin

Gradle convention plugins are essential tools for streamlining your workflow and ensuring consistency across your projects. They allow you to define standardized sets of configurations for your Gradle builds, which can then be applied to multiple projects or modules.

Imagine defining conventions for Java version, common libraries, and test tasks for your Android projects. A Gradle convention plugin enables you to encapsulate these configurations and apply them automatically to each project, eliminating the need to repeat the same tedious configurations.

Here are some key benefits of Gradle convention plugins:

  • Project Consistency: Ensure all your projects share a uniform build structure and configurations, making maintenance and collaboration easier.
  • Reduced Code Duplication: Eliminate redundancies in your Gradle build scripts, focusing only on project-specific configurations.
  • Improved Readability: Create clearer and more understandable build scripts by organizing conventions logically.
  • Simplified Maintenance: Make it easier to update build configurations for your entire suite of projects by centralizing changes in the convention plugin.

If you develop multiple Android apps, Gradle convention plugins are a valuable asset for streamlining your build process and maintaining consistent code quality.

In summary, Gradle convention plugins are powerful tools for standardizing and simplifying Gradle build configurations, leading to improved productivity and maintainability for your Android projects.

JaCoCo

Jacoco is a popular open-source code coverage tool specifically designed for Java applications. It provides detailed insights into the portions of code that are executed during testing, helping developers identify any untested areas and improve overall code quality.

JaCoCo operates by instrumenting Java bytecode, adding counters to track the execution of each code block. This data is then collected and analyzed to generate comprehensive code coverage reports.

Key features of JaCoCo include:

  • Line coverage: Measures the percentage of code lines that have been executed.
  • Branch coverage: Assesses the coverage of conditional branches (if-else statements, switch statements).
  • Method coverage: Determines the proportion of methods that have been invoked.
  • HTML and XML reports: Generates detailed reports in HTML and XML formats, visualizing coverage data with color-coded bars and charts.

For Android developers, JaCoCo is an invaluable tool for ensuring thorough test coverage and identifying potential gaps in testing strategies. It helps developers write more robust and reliable Android applications.

In essence, JaCoCo serves as a valuable ally for Android developers, providing crucial insights into code coverage and driving continuous improvement in software quality.

How to create a Jacoco Gradle Convention Plugin?

Create a directory named build-logic on the root of the project (see composite builds)

  1. create a subdirectory named convention or any other else (empty for now)

2. Add a gradle.properties file that contains

3. Add a settings.gradle.kts file that contains

4. Submodule convention structure

You must have this structure for beginning develop our jacoco convention gradle plugin

6. Create a file build.gradle.kts

7. Add a file ProjectExt.kt

This file contains the Project class extension to make it easier to get the version catalog file.

You can add other version catalog with different name of course.

8. Create the JacocoExt File

This class contains coverage exclusions that is a list of pattern file that you want to exclude of the coverage code.

private val coverageExclusions = listOf(
// Android
"**/R.class",
"**/R\$*.class",
"**/BuildConfig.*",
"**/Manifest*.*"
)

this extension of Project contains a method configureJacoco() that we will explain now.

first we get the android components extension (for have the variants and the flavors of the module when the plugin jacoco will be applied)

val androidComponentsExtension = extensions
.getByType(AndroidComponentsExtension::class.java)

After we configure the jacoco plugin to set the tool version (version and module must be added into version catalog)

configure<JacocoPluginExtension> {
toolVersion = this@configureJacoco.libs.findVersion("jacoco").get().toString()
}

//libs.versions.toml
//[versions]
//jacoco = "0.8.12"
//[libraries]
//jacoco = { group = "org.jacoco", name = "org.jacoco.core", version.ref = "jacoco" }

After for each flavor and buildVariant it will create the report task for each testTask. Every report task depends on testTask.

The report task generate an xml and html coverage code file.

To calculate the coverage code jacoco must have :

  • the kotlin-classes generated from each flavor/buildVariant without the files that you want to exclude.
val filesFilters = layout.buildDirectory.dir("tmp/kotlin-classes/${variant.name}").get().asFileTree.matching {
exclude(coverageExclusions)
}
classDirectories.setFrom(filesFilters)
  • the sources code of the module
sourceDirectories.setFrom(files("$projectDir/src/main/java", "$projectDir/src/main/kotlin"))
  • and the excution test data result
val executionDataVariant = layout.buildDirectory.file("/outputs/unit_test_code_coverage/${variant.name}UnitTest/${testTaskName}.exec").get().asFile
executionData.setFrom("${layout.buildDirectory.get()}$executionDataVariant")

This is the complete code of the loop for each flavor/variant

androidComponentsExtension.onVariants { variant ->
val testTaskName = "test${variant.name.capitalize()}UnitTest"
val reportTask = tasks.register("jacoco${testTaskName.capitalize()}Report", JacocoReport::class.java) {
group = "reporting"
dependsOn(testTaskName)
reports {
xml.required.set(true)
html.required.set(true)
}

val filesFilters = layout.buildDirectory.dir("tmp/kotlin-classes/${variant.name}").get().asFileTree.matching {
exclude(coverageExclusions)
}
classDirectories.setFrom(filesFilters)
sourceDirectories.setFrom(files("$projectDir/src/main/java", "$projectDir/src/main/kotlin"))
val executionDataVariant = layout.buildDirectory.file("/outputs/unit_test_code_coverage/${variant.name}UnitTest/${testTaskName}.exec").get().asFile
executionData.setFrom("${layout.buildDirectory.get()}$executionDataVariant")
}

jacocoTestReport.dependsOn(reportTask)
}

If you use robolectric for ui test you must add these lines to exclude jdk.internal

tasks.withType<Test>().configureEach {
configure<JacocoTaskExtension> {
// Required for JaCoCo + Robolectric
// https://github.com/robolectric/robolectric/issues/2230
// Consider removing if not we don't add Robolectric
isIncludeNoLocationClasses = true

// Required for JDK 11 with the above
// https://github.com/gradle/gradle/issues/5184#issuecomment-391982009
excludes = listOf("jdk.internal.*")
}
}

This method creates the gradle tasks that will appear into reporting group (here we have 3 flavors (dev, preProd and production) and variant debug and release.

Creation of the jacoco convention gradle plugin

Create a file android-jacoco-convention.gradle.kts into your package desired (ex: com.example.jacoco). The name of the file determines the plugin id (here : android-jacoco-convention) that you must add into plugins section on each module you want to get the coverage code.

This file apply the jacoco plugin and configure jacoco for each flavor/buildVariant

plugins {
id("jacoco")
}


project.configureJacoco()

10. Update settings.gradle.kts on the root project

add includeBuild("build-logic") into pluginManagement block.

How to apply the convention?

for each all modules (android application or android library), add this line id("android-jacoco-convention") to build.gradle.kts into the plugins section

for android application

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("android-jacoco-convention")
}

for android library

plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("android-jacoco-convention")
}

Launch the gradle tasks

To launch the task created by the Jacoco Convention Plugin, execute this command gradle (ex : for flavor Dev and build variant Debug)

launch the gradle task gradle jacocoTestDevDebugUnitTestReport

Result:

It genererate a report into /build/reports/jacoco for each module (in our example we have applied the convention to app module and log module).

For module app

We see that even the compose code tested with robolectric has been covered.

For log module

Bonus : Aggregate reports

To aggregate report, I recommand the plugin:

io.github.gmazzo.test.aggregation.coverage

for more explanations see

and this article :

Into the build.gradle.kts of the root project just add this line alias(libs.plugins.jacoco.aggreate)

plugins {
alias(libs.plugins.jetbrains.kotlin.android) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.jacoco.aggreate)
}

/*
libs.versions.toml
[versions]
jacocoAggregate = "2.2.0"
[plugins]
jacoco-aggreate = { id = "io.github.gmazzo.test.aggregation.coverage", version.ref = "jacocoAggregate"}
*/

This plugin creates a gradle task named jacocoAggregatedReport

A report will be generated into rootProject/build/reports/jacoco that contains all results aggregated.

For more information about conventions gradle plugin

--

--