Code coverage abstraction
| |

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

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.

Similar Posts

  • | |

    Retrieval Augmented Generation with Spring AI

    In our last post, we looked at enriching the OpenAI model with custom data through function calls. While this technique is useful, it has its limitations and performance trade-offs. Today, we explore a more efficient way of incorporating relevant data into prompts to receive accurate and relevant model responses. Retrieval Augmented Generation, or RAG, relies on preprocessed data that is readily available upon request. In this post, we will build an Extract, Transform, Load (ETL) pipeline that stores a large corpus of weather forecasts and learn how to efficiently retrieve relevant information from a vector store.

  • Embracing Efficiency with Kotlin Coroutines

    Kotlin coroutines are a powerful tool that propels efficiency in dealing with concurrent programming. They enable to treat asynchronous code as if it was synchronous, making it easier to manage and understand. In this blog post, I explore some of the core principles of coroutines and compare their performance against conventional blocking calls.

  • The power of sealed interfaces in Kotlin

    I’m a strong advocate of the separation of logic principle. In the service layer, we often deal with various I/O operations and interactions with external systems. Not only is it essential to correctly handle exceptions but also make a clear distinction between successful responses and different error scenarios. Kotlin offers sealed classes and interfaces as a powerful tool to establish a predefined hierarchy of subtypes. In this post, I will show you how this feature has proven invaluable, allowing me to keep my code concise without the burden of excessive boilerplate.

  • |

    Conquer Authentication with Ktor: Part 8 – Protect Access with CORS

    Ensuring security and flexibility of web services when it comes to cross-origin resource sharing is essential. This is elegantly managed through the implementation of Cross-Origin Resource Sharing (CORS), an established practice for modern web applications. A well defined CORS policy not only enhances security but also promotes a seamless interaction between different domains. Thankfully, Ktor makes this process straightforward and efficient. In this final part of our series on authentication with Ktor, we will provide clear examples to guide you. By the end of this post, you’ll see how effortless it is to integrate CORS into your Ktor projects, ensuring your services are both secure and accessible.

  • | |

    Conquer Authentication with Ktor: Part 6 – Implementing JSON Web Tokens

    Welcome back to our journey with the Ktor framework. Our previous post introduced you to JSON Web Tokens (JWT) and their impact on authentication in modern web applications. You learned about the key benefits of JWT, such as statelessness, improved scalability, cross-platform compatibility, and enhanced security. Today, we take things a step further with a hands-on approach, showing you how to effectively implement JWT using Ktor. Follow along as we dive into the practical side of JWT with Ktor to secure your web application seamlessly and effectively. By the end of this post, you’ll have a deeper understanding of how JWT and Ktor work together to create a robust and maintainable security model.

  • | |

    Streamlining Console Output Verification with Kotest

    Console logging is often perceived as a bad practice, but in specific contexts, it’s quite suitable. Consider a scenario of a workshop where printing output directly to the console enhances transparency and simplifies the project setup. Using a comprehensive logger could lead to unnecessary clutter. However, there’s a downside to directly printing to the console—it makes it challenging to verify the result. Well, at least so I thought until I accidentally stumbled upon this awesome article by Thijs Kaper. Although Spring Boot provides out-of-the-box support for output verification, it’s also possible, and sometimes preferable, to implement it independently. In this blog post, I’ll demonstrate how to create a custom spec in Kotest that you can leverage in your tests. This spec automatically captures console output and provides the result for verification.