Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save shubhendras11/d366717985ca5eae776bfbb153c5d1a0 to your computer and use it in GitHub Desktop.

Select an option

Save shubhendras11/d366717985ca5eae776bfbb153c5d1a0 to your computer and use it in GitHub Desktop.
JaCoCo Code Coverage Report for Multi-Module Android Project

JaCoCo Code Coverage Report for Multi-Module Android Project

This guide explains how to set up and generate a complete code coverage report for a multi-module Android project using JaCoCo. It includes both unit and instrumented tests.

Steps to Implement JaCoCo in Your Project

1. Apply JaCoCo Plugin in All Modules

To begin, apply the JaCoCo plugin in all modules of your project (including the root module)

plugins {
    jacoco
}

2. Enable Coverage for UI and Unit Tests

Next, enable coverage for both unit and UI tests in the android { ... } block of your app/build.gradle file:

android {
    buildTypes {
        debug {
            enableAndroidTestCoverage = true
            enableUnitTestCoverage = true
        }
    }
}

3. Add JaCoCo Report Task in Project-Level build.gradle

In your project-level build.gradle, add the following script to generate the JaCoCo coverage reports for all modules (including both unit and instrumented tests):

// =====================
// JaCoCo Report Task
// =====================
tasks.register<JacocoReport>("jacocoFullCoverageReportAllModules") {
    group = "Reports"
    description = "Generate JaCoCo coverage reports (Unit + Instrumented) for all modules"

    val fileFilter = listOf(
        // Android-specific generated files
        "**/R.class",
        "**/R$*.class",
        "**/BuildConfig.*",
        "**/Manifest*.*",
        "**/resources/**",
        "**/values/**",

        // Test files
        "**/*Test*.*",
        "**/*Test$*.*",
        "**/androidTest/**",
        "**/test/**",

        // Hilt/Dagger-generated code
        "**/hilt_aggregated_deps/**",
        "**/dagger/hilt/internal/**",
        "**/dagger/hilt/android/internal/**",
        "**/*_MembersInjector.class",
        "**/Dagger*Component.class",
        "**/*Module_*Factory.class",
        "**/*_Factory.class",
        "**/*_Provide*Factory.class",
        "**/*_Impl.class",

        // Kotlin-generated classes
        "**/*\$Lambda$*.*",
        "**/*\$inlined$*.*",
        "**/*\$*.*", // anonymous classes and lambdas
        "**/Companion.class",

        // Navigation safe args (generated)
        "**/*Directions*.class",
        "**/*Args.class",

        // Jetpack Compose compiler-generated
        "**/*Preview*.*",
        "**/*ComposableSingletons*.*",

        // Room and other annotation processors
        "**/*_Impl.class",
        "**/*Serializer.class", // For Moshi, Retrofit, etc.

        // Miscellaneous
        "android/**/*.*",

        // Project-specific exclusions
        "**/di/**",
        "**/state/**",
        "**/mapper/**",
        "**/domain/**"
    )

    val javaClasses = mutableListOf<FileTree>()
    val kotlinClasses = mutableListOf<FileTree>()
    val javaSrc = mutableListOf<String>()
    val kotlinSrc = mutableListOf<String>()
    val execution = mutableListOf<FileTree>()

    rootProject.subprojects.forEach { proj ->
        proj.tasks.findByName("testDebugUnitTest")?.let { dependsOn(it) }
        proj.tasks.findByName("connectedDebugAndroidTest")?.let { dependsOn(it) }

        javaClasses.add(proj.fileTree("${proj.buildDir}/intermediates/javac/debug") {
            exclude(fileFilter)
        })

        kotlinClasses.add(proj.fileTree("${proj.buildDir}/tmp/kotlin-classes/debug") {
            exclude(fileFilter)
        })

        javaSrc.add("${proj.projectDir}/src/main/java")
        kotlinSrc.add("${proj.projectDir}/src/main/kotlin")

        execution.add(proj.fileTree("${proj.buildDir}") {
            include(
                "jacoco/testDebugUnitTest.exec", // Unit test,
                "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec", // Unit test
                "outputs/code_coverage/debugAndroidTest/connected/**/*.ec" // UI test
            )
        })
    }

    sourceDirectories.setFrom(files(javaSrc + kotlinSrc))
    classDirectories.setFrom(files(javaClasses + kotlinClasses))
    executionData.setFrom(files(execution))

    reports {
        xml.required.set(true)
        html.required.set(true)
        html.outputLocation.set(file("${rootProject.buildDir}/coverage-report"))
    }

    doLast {
        println("βœ… Combined coverage report generated at:")
        println("πŸ“„ file://${reports.html.outputLocation.get()}/index.html")
    }
}

4. Create Task to Run All Tests and Generate Reports

To run all unit and UI tests across all modules and generate the combined JaCoCo coverage report, add the following task to the project-level build.gradle:

// =============================
// Run-All Test + Report Task
// =============================
tasks.register("runAllCoverageAndReport") {
    group = "Verification"
    description = "Runs unit + UI tests across all modules and generates a full Jacoco report"

    val testTaskPaths = mutableListOf<String>()

    rootProject.subprojects.forEach { proj ->
        proj.tasks.matching { it.name == "testDebugUnitTest" }.forEach {
            testTaskPaths.add(it.path)
        }
        proj.tasks.matching { it.name == "createDebugCoverageReport" }.forEach {
            testTaskPaths.add(it.path)
        }
    }

    dependsOn(testTaskPaths)
    dependsOn("jacocoFullCoverageReportAllModules")

    doFirst {
        println("Running test tasks:")
        testTaskPaths.forEach { println(" - $it") }
    }
}

5. Sync Gradle and Run the Report Task

Finally, sync your Gradle files and run the following command to execute the tests and generate the JaCoCo report:

./gradlew runAllCoverageAndReport

Output

Once the process is complete, the combined JaCoCo coverage report will be available at the following location:

πŸ“„ file://<rootProject.buildDir>/coverage-report/index.html

You can open this file in any browser to view detailed coverage metrics for both unit and instrumented tests across all modules in your project.


Note

Scipt will fail if any of your module contain an empty androidTest or test folder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment