Step-by-Step JaCoCo Setup for Android Code Coverage

Step-by-step guide to integrating Jacoco with GitLab CI/CD for seamless auto coverage reports.
Sep 26 2021 · 3 min read

Background

In the previous story, we set up Git CI/CD + Fastlane for auto-deployment. We now going to go through how to set up the Jacoco plugin and integrate it in .gitlab-ci.yml for the auto coverage test report.

Please have a look at the previous/next stories related to gitlab and CI for more information in case you haven’t yet.

Part 1:

Part 2:

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!

What is JaCoCo?

From documentation:

JaCoCo should provide the standard technology for code coverage analysis in Java VM based environments. The focus is on providing a lightweight, flexible and well-documented library for integration with various build and development tools.

Let’s get started on how we can integrate JaCoCo in an android project and run the verification tests on CI.

1. Set up Jacoco Plugin

Let’s add a plugin at the project level build.gradle file.

buildscript {
    repositories { 
        google()
        ...
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:7.0.0'
        //Jacoco Plugin
        classpath "org.jacoco:org.jacoco.core:0.8.7"
    }
}

2. Set up Jacoco configuration

We’re going to create a separate Gradle file for Jacoco to avoid large configurations in the module’s build.gradle file.

Hers’s jacoco.gradle file:

apply plugin: 'jacoco'
jacoco {
    toolVersion = "0.8.7"
}
project.afterEvaluate {
    if (android.hasProperty("applicationVariants")) {
        android.applicationVariants.all { variant ->
            createVariantCoverage(variant)
        }
    } else if (android.hasProperty("libraryVariants")) {
        android.libraryVariants.all { variant ->
            createVariantCoverage(variant)
        }
    }
}
ext.excludes = [
                  '**/databinding/*Binding.*',
                  '**/R.class',
                  '**/R$*.class',
                  '**/BuildConfig.*',
                  '**/Manifest*.*',
                  '**/*Test*.*',
                  'android/**/*.*',
                  // butterKnife
                  '**/*$ViewInjector*.*',
                  '**/*$ViewBinder*.*',
                  '**/Lambda$*.class',
                  '**/Lambda.class',
                 '**/*Lambda.class',
                 '**/*Lambda*.class',                
                  '**/*_MembersInjector.class',
                  '**/Dagger*Component*.*',
                  '**/*Module_*Factory.class',
                  '**/di/module/*',
                  '**/*_Factory*.*',
                  '**/*Module*.*',
                  '**/*Dagger*.*',
                  '**/*Hilt*.*',
                  // kotlin
                  '**/*MapperImpl*.*',
                  '**/*$ViewInjector*.*',
                  '**/*$ViewBinder*.*',
                  '**/BuildConfig.*',
                  '**/*Component*.*',
                  '**/*BR*.*',
                  '**/Manifest*.*',
                  '**/*$Lambda$*.*',
                  '**/*Companion*.*',
                  '**/*Module*.*',
                  '**/*Dagger*.*',
                  '**/*Hilt*.*',
                  '**/*MembersInjector*.*',
                  '**/*_MembersInjector.class',
                  '**/*_Factory*.*',
                  '**/*_Provide*Factory*.*',
                  '**/*Extensions*.*'
            ]
def createVariantCoverage(variant) {
    def variantName = variant.name
    def testTaskName = "test${variantName.capitalize()}UnitTest"

    // Add unit test coverage tasks
    tasks.create(name: "${testTaskName}Coverage", type: JacocoReport, dependsOn: "$testTaskName") {
        group = "Reporting"
        description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build."

        reports {
            html.enabled = true
        }

        def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir, excludes: project.excludes)
        def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: project.excludes)
        getClassDirectories().setFrom(files([javaClasses, kotlinClasses]))

        getSourceDirectories().setFrom(files([
                "$project.projectDir/src/main/java",
                "$project.projectDir/src/${variantName}/java",
                "$project.projectDir/src/main/kotlin",
                "$project.projectDir/src/${variantName}/kotlin"
        ]))

        getExecutionData().setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/${variantName}UnitTest/${testTaskName}.exec"))

        doLast {
            def m = new File("${project.buildDir}/reports/jacoco/${testTaskName}Coverage/html/index.html")
                    .text =~ /Total[^%]*>(\d?\d?\d?%)/
            if (m) {
                println "Test coverage: ${m[0][1]}"
            }
        }
    }

    // Add unit test coverage verification tasks
    tasks.create(name: "${testTaskName}CoverageVerification", type: JacocoCoverageVerification, dependsOn: "${testTaskName}Coverage") {
        group = "Reporting"
        description = "Verifies Jacoco coverage for the ${variantName.capitalize()} build."
        violationRules {
            rule {
                limit {
                    minimum = 0
                }
            }
            rule {
                element = 'BUNDLE'
                limit {
                    counter = 'LINE'
                    value = 'COVEREDRATIO'
                    minimum = 0.30
               }
            }
        }
        def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir, excludes: project.excludes)
        def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: project.excludes)
        getClassDirectories().setFrom(files([javaClasses, kotlinClasses]))
        getSourceDirectories().setFrom(files([
                "$project.projectDir/src/main/java",
                "$project.projectDir/src/${variantName}/java",
                "$project.projectDir/src/main/kotlin",
                "$project.projectDir/src/${variantName}/kotlin"
        ]))
        getExecutionData().setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/${variantName}UnitTest/${testTaskName}.exec"))
    }
}

Let’s break it down

reports {
    html.enabled = true
}

Specify the type of report we want to generate.

def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir, excludes: project.excludes)
def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: project.excludes)

These two lines tell Jacoco about our kotlin and java classes.

excludes contain a list of all classes and packages which we’re not going to cove in our test coverage.

getExecutionData().setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/${variantName}UnitTest/${testTaskName}.exec"))

Specify the path of .execfile to find execution data.

You can find a detailed explanation of Jacoco properties HERE.

3: Apply Jacoco plugin

apply plugin: 'com.android.application'
...
apply from: '../jacoco.gradle'

Run ./gradlew testDevDebugUnitTestCoverageVerification

And, you will see generated report in the Jacoco directory.

4. Enable coverage

//App build.gradle
buildTypes {
    debug{
        testCoverageEnabled true
    }
}

5. Configure Git CI

To automate Jacoco converge verification, we’re going to add a job to run coverage verification on our behalf.

stages:
  - coverageTest
  - build
build-test:
  stage: coverageTest
  before_script:
    - chmod +x ./gradlew
    - export GRADLE_USER_HOME=$PWD/.gradle
  script:
    - ./gradlew app:testDevDebugUnitTestCoverageVerification

and that’s it. Push some code and Run your pipeline!


radhika-s image
Radhika saliya
Android developer | Sharing knowledge of Jetpack Compose & android development


radhika-s image
Radhika saliya
Android developer | Sharing knowledge of Jetpack Compose & android development


Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.