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.
To begin, apply the JaCoCo plugin in all modules of your project (including the root module)
plugins {
jacoco
}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
}
}
}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")
}
}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") }
}
}Finally, sync your Gradle files and run the following command to execute the tests and generate the JaCoCo report:
./gradlew runAllCoverageAndReportOnce 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.
Scipt will fail if any of your module contain an empty androidTest or test folder.