Unlocking Test Coverage in Kotlin Multiplatform with JaCoCo and GitHub Actions – Part 1
Kotlin Multiplatform unlocks new possibilities for developing cross-platform applications. However, this innovative approach does not come without its complexities. Especially when delving into its specific Gradle configuration. One of the challenges is establishing a reliable, automated test coverage report – an integral part of maintaining code quality and integrity. In this mini-series, we’ll learn how to leverage JaCoCo Gradle plugin to generate code coverage reports. Additionally, we’ll leverage GitHub Actions to generate a coverage badge independently from any third-party platform. This approach simplifies the process by keeping everything within the GitHub ecosystem, providing a seamless workflow for your multi platform projects.
Test coverage is a metric used to measure the amount of code that is executed when automated tests run. It helps identify untested parts of your codebase, ensuring that the critical paths and functionalities are validated through tests. This is especially crucial for Kotlin Multiplatform projects, whose code base spans multiple platforms (such as JVM, web, iOS, Android etc.). Given the diverse range of platforms and the inherent complexity in coding for them, achieving high test coverage ensures that the shared codebase works as expected. This not only boosts the confidence in the code’s reliability and quality but also streamlines the development process by catching bugs early, reducing the likelihood of platform-specific issues, and facilitating easier maintenance and updates of your application.
Table of Contents
- TL;DR
- Why to Use JaCoCo in Kotlin Multiplatform Projects?
- The Starting Point
- Step 1 – Add and Configure JaCoCo Plugin
- Step 2 – Register A Report Task
- Step 3 – Define The Monitored Code Base
- Step 4 – Help JaCoCo Understand Your Code Base
- Step 5 – Define The Report Format
- The Complete Template
- Summary
TL;DR
If you are just looking for a quick solution that enables test coverage in your Kotlin Multiplatform project, explore my complete template.
Why to Use JaCoCo in Kotlin Multiplatform Projects?
The JaCoCo Gradle plugin is a popular tool that integrates seamlessly with Gradle projects, including those written in Kotlin, to measure code coverage. It provides detailed reports on how much code is being executed in tests, highlighting what’s been covered and what’s been missed. Kotlin Multiplatform projects include shared logic as well as platform-specific intricacies. JaCoCo offers invaluable insights that help developers write more comprehensive tests and maintain high code quality.
The Starting Point
Here is a skeleton of build.gradle.kts
of an KMP project that targets JVM and JavaScript platforms.
plugins {
alias(libs.plugins.kotlinMultiplatform)
id("module.publication")
id("org.jetbrains.kotlin.plugin.serialization")
}
kotlin {
jvm {
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
js(IR) {
moduleName = "zoomsdk"
browser {
webpackTask {
output.libraryTarget = "umd"
}
}
nodejs {}
binaries.executable()
}
sourceSets {
val commonMain by getting {
dependencies {
// Platform independent libraries
}
}
val commonTest by getting {
dependencies {
// Platform independent test libraries
}
}
val jvmMain by getting {
dependencies {
// JVM specific libraries
}
}
val jvmTest by getting {
dependencies {
// JVM specific test libraries, such as JUnit
}
tasks.withType<Test> {
useJUnitPlatform()
testLogging {
events("passed", "skipped", "failed")
}
}
}
val jsMain by getting {
dependencies {
// JS specific dependencies
}
}
val jsTest by getting {
dependencies {
// JS specific test dependencies
}
}
}
}
Curious about why I specifically chose this setup and where it all started? Feel free to check out the original project on GitHub. I’ll save the shameless plug for another day 😉
Step 1 – Add and Configure JaCoCo Plugin
First of all, add jacoco
in the plugin
section.
plugins {
jacoco
// your other plugins
}
Next, make sure to use the latest version (0.8.12
as of time of this writing) and optionally override the directory where the test coverage reports will be generated. The default location is build/reports/jacoco
. In the example below I could’ve skipped declaring it, but let’s just leave it in for illustration purposes.
jacoco {
toolVersion = "0.8.12"
reportsDirectory = layout.buildDirectory.dir("reports/jacoco")
}
Step 2 – Register A Report Task
To ensure that test coverage reports are automatically generated each time tests are executed during the build process, we need to configure Gradle accordingly. This involves defining a new task within the build script. We will name this task jacocoTestReport
, aligning with the convention used by the JaCoCo plugin for Gradle, which facilitates the creation of these reports.
tasks.register("jacocoTestReport", JacocoReport::class) {
dependsOn(tasks.withType(Test::class))
}
To enable automatic generation of the coverage report by Gradle, we must establish a dependency relationship. This is done by declaring that our custom task, jacocoTestReport
, relies on the successful completion of the test
task. Furthermore, we must ensure that Gradle generates the report immediately after the JVM tests conclude. To accomplish this, we integrate a finalizedBy
configuration block within our test
task setup.
val jvmTest by getting {
dependencies {
// JVM specific test libraries, such as JUnit
}
tasks.withType<Test> {
finalizedBy(tasks.withType(JacocoReport::class))
// The rest of the usual configuration
}
}
Step 3 – Define The Monitored Code Base
JaCoCo is primarily designed for Java, which makes it an excellent choice for projects implemented in Kotlin, given Kotlin’s seamless compatibility with the JVM. This ensures comprehensive code coverage tracking for both Java and Kotlin codebases. However, for JavaScript and web development projects, we might consider other tools more tailored to the specific challenges and needs of the web ecosystem.
Therefore, let’s instruct JaCoCo to monitor our common code base, along with the code specific to the JVM.
tasks.register("jacocoTestReport", JacocoReport::class) {
dependsOn(tasks.withType(Test::class))
val coverageSourceDirs = arrayOf(
"src/commonMain",
"src/jvmMain"
)
}
Step 4 – Help JaCoCo Understand Your Code Base
In the JaCoCo Gradle plugin, sourceDirectories
and classDirectories
have an essential role in determining what code is considered when evaluating test coverage.
The sourceDirectories
property specifies the directories containing the source code for the project. The JaCoCo plugin uses these directories to locate the source files that your tests are targeting. In the previous step we’ve declared a custom variable coverageSourceDirs
that points to our common and JVM source code.
The classDirectories
property specifies the directories containing the compiled class files. These class files are the actual bytecode that runs when your program is executed. By monitoring these during testing, JaCoCo can accurately determine which pieces of code are executed during testing and which aren’t.
Here is the resulting configuration.
val buildDir = layout.buildDirectory
// Include all compiled classes.
val classFiles = buildDir.dir("classes/kotlin/jvm").get().asFile
.walkBottomUp()
.toSet()
// This helps with test coverage accuracy.
classDirectories.setFrom(classFiles)
sourceDirectories.setFrom(files(coverageSourceDirs))
// The resulting test report in binary format.
// It serves as the basis for human-readable reports.
buildDir.files("jacoco/jvmTest.exec").let {
executionData.setFrom(it)
}
Step 5 – Define The Report Format
JaCoCo is capable of producing test coverage reports in HTML, XML, or CSV formats. Depending on your specific needs, you can use any one of these formats, or even all of them.
reports {
xml.required = true
html.required = true
}
The Complete Template
To simplify the integration of test coverage reporting into Kotlin Multiplatform projects, I’ve developed a template that encapsulates all the configurations discussed in this post. This includes automated report generation and setup for multi-platform testing scenarios. You’re invited to explore and use this template to streamline your project setup. Find it on my GitHub, and enhance your development workflow.
Summary
Kotlin Multiplatform significantly enhances developer productivity and reduces the possibility of human error. In this post, we’ve delved into advancing our project by integrating test coverage reports. In the upcoming second part of this mini-series, we will delve into incorporating test coverage reports into our CI/CD processes using GitHub Actions. By the conclusion of this tutorial, you’ll achieve a notable milestone: Showcasing a custom test coverage badge in your README file, achieved without requiring any third-party services. Stay tuned for more insights and thank you for reading.