How to configure JaCoCo for Kotlin & Java project

Introduction

Here’s the description of JaCoCo from the official website:

JaCoCo is a free code coverage library for Java, which has been created by the EclEmma team based on the lessons learned from using and integration existing libraries for many years.

There are many articles which show how to configure JaCoCo on a Java project. But there aren’t many articles how to do that on a Kotlin or Java + Kotlin project.

It’s quite an important topic, as many developers start migrating their projects from Java to Kotlin (especially in Android world). Having no coverage upsets people a lot.

I have spent some time on this configuration and turns out that configuration is very simple.

JaCoCo configuration on Kotlin/Java project

Basic configuration of JaCoCo plugin using Gradle can be found in Gradle JaCoCo userguide. Nothing interesting here and configuring it on Java project should be very straightforward.

As for Kotlin, I’ll use Android project as an example as I’m mostly Android developer. You can find JaCoCo configuration in one of my Github projects (ci-matters, written in Java).

apply plugin: 'jacoco'

ext {
    coverageSourceDirs = 'src/test/java'
}

jacoco {
    toolVersion = "0.7.5.201505241946"
    reportsDir = file("$buildDir/reports")
}

task jacocoTestReport(type:JacocoReport, dependsOn: "testDebugUnitTest") {
    group = "Reporting"
    description = "Generate Jacoco coverage reports for Debug build"
    
    reports {
        xml.enabled = true
        html.enabled = true
    }
    
    classDirectories = fileTree(
            dir: "$buildDir/intermediates/classes/debug",
            excludes: ['**/R.class',
                       '**/R$*.class',
                       '**/*$ViewBinder*.*',
                       '**/*$InjectAdapter*.*',
                       '**/*Injector*.*',
                       '**/BuildConfig.*',
                       '**/Manifest*.*',
                       '**/*Test*.*',
                       '**/*Activity*.*',
                       '**/CiMattersApplication*.*',
                       'android/**/*.*']
    )
    
    if (project.hasProperty("teamcity")) {
        println '##teamcity[jacocoReport dataPath=\'app/build/jacoco/testDebugUnitTest.exec\' includes=\'com.vgaidarji.cimatters.*\' excludes=\'com.vgaidarji.cimatters.test.* **/*R*.* **/*Injector*.* **/*Activity*.* .*R .*CiMattersApplication .*BuildConfig .*Activity .*Test \']'
    }
    
    additionalSourceDirs = files(coverageSourceDirs)
    sourceDirectories = files(coverageSourceDirs)
    executionData = files("$buildDir/jacoco/testDebugUnitTest.exec")
    
    // Bit hacky but fixes https://code.google.com/p/android/issues/detail?id=69174.
    // We iterate through the compiled .class tree and rename $$ to $.
    doFirst {
        new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
            if (file.name.contains('$$')) {
                file.renameTo(file.path.replace('$$', '$'))
            }
        }
    }
}

This is more or less standard way of configuring JaCoCo on Android project and almost everyone does it this way.

Let’s see what is different for Java + Kotlin project.

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.9"
    reportsDir = file("$buildDir/reports")
}

task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") {
    group = "Reporting"
    description = "Generate Jacoco coverage reports for Debug build"

    reports {
        xml.enabled = true
        html.enabled = true
    }

    // what to exclude from coverage report
    // UI, "noise", generated classes, platform classes, etc. 
    def excludes = [
            '**/R.class',
            '**/R$*.class',
            '**/*$ViewInjector*.*',
            '**/BuildConfig.*',
            '**/Manifest*.*',
            '**/*Test*.*',
            'android/**/*.*',
            '**/*Fragment.*',
            '**/*Activity.*'
    ]
    // generated classes
    classDirectories = fileTree(
            dir: "$buildDir/intermediates/classes/debug",
            excludes: excludes
    ) + fileTree(
            dir: "$buildDir/tmp/kotlin-classes/debug",
            excludes: excludes
    )

    // sources
    sourceDirectories = files([
            android.sourceSets.main.java.srcDirs,
            "src/main/kotlin"
    ])
    executionData = files("$buildDir/jacoco/testDebugUnitTest.exec")
}

The “trick” here is to specify where are located Java/Kotlin generated classes and sources. Here we combine 2 folders:

fileTree(
        // Java generated classes on Android project (debug build)
        dir: "$buildDir/intermediates/classes/debug",
        excludes: excludes
) + fileTree(
        // Kotlin generated classes on Android project (debug build)
        dir: "$buildDir/tmp/kotlin-classes/debug",
        excludes: excludes
)

Here we specify where are our source Java/Kotlin classes located:

sourceDirectories = files([
        android.sourceSets.main.java.srcDirs,
        "src/main/kotlin"
])

That’s it! As result, we will have combined JaCoCo report generated once we run our tests and gather code coverage using ./gradlew testDebugUnitTest jacocoTestReport command. It should work for any type of Java/Kotlin project, not only Android.

Worth mentioning, that I didn’t find any issues so far with JaCoCo plugin on Kotlin/Java project. Maybe there are none. But I wouldn’t be surprised if one day someone builds KoCoCo :chicken: :trollface: (Kotlin Code Coverage tool).

comments powered by Disqus