Jacoco generates test reports only for instrumented tests but not for unit tests

I am trying to integrate jacoco to my build.gradle.kts:

import org.jlleitschuh.gradle.ktlint.reporter.ReporterType

plugins {
    // ...
    id("jacoco")
}

android {
    // ...
    buildTypes {
        debug {
            enableUnitTestCoverage = true
            enableAndroidTestCoverage = true
        }
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro",
            )
        }
    }
    testOptions {
        unitTests {
            isIncludeAndroidResources = true
            isReturnDefaultValues = true
            all { test ->
                test.testLogging {
                    showStandardStreams = true

                    events("started", "passed", "skipped", "failed", "standard_out", "standard_error")
                    exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL
                }
            }
        }
    }
}

jacoco {
    toolVersion = libs.versions.jacocoVersion.get()     // 0.8.13
}

tasks.register<JacocoReport>("jacocoTestReport") {
    dependsOn("testDebugUnitTest", "createDebugCoverageReport")

    reports {
        html.required.set(true)
        xml.required.set(false)
        csv.required.set(false)
    }

    val fileFilter = listOf(
        "**/R.class",
        "**/R$*.class",
        "**/Manifest*.*",
        "**/*Test*.*",
        "android/**/*.*",
        "**/*MapperImpl*.*",
        "**/*\$ViewInjector*.*",
        "**/*\$ViewBinder*.*",
        "**/BuildConfig.*",
        "**/*Component*.*",
        "**/*BR*.*",
        "**/*\$Lambda$*.*",
        "**/*Companion*.*",
        "**/*Module*.*",
        "**/*Dagger*.*",
        "**/*Hilt*.*",
        "**/*MembersInjector*.*",
        "**/*_MembersInjector.class",
        "**/*_Factory*.*",
        "**/*_Provide*Factory*.*",
        "**/*Extensions*.*",
        "**/*Koin*.*",
        "androidx/databinding/**/*.*",
        "**/databinding/*Binding.*",
        "**/*ComposableSingletons*.*",
        "**/*Kt$*"
    )

    val debugTree = fileTree("${layout.buildDirectory}/intermediates/classes/debug") {
        exclude(fileFilter)
    }
    val mainSrc = "${project.projectDir}/src/main/java"
    val kotlinSrc = "${project.projectDir}/src/main/kotlin"
    sourceDirectories.setFrom(files(mainSrc, kotlinSrc).filter { it.exists() })
    classDirectories.setFrom(files(debugTree).filter { it.exists() })
    executionData.setFrom(
        files("${layout.buildDirectory}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec").filter { it.exists() }
    )

    doLast {
        if (reports.html.outputLocation.get().asFile.exists()) {
            println("JaCoCo Unit Test Report generated at: file://${reports.html.outputLocation.get().asFile}/index.html")
        } else {
            println("JaCoCo HTML report was NOT generated. Check previous logs for errors.")
            println("Execution data files found: ${executionData.files.size}")
            executionData.files.forEach {
                println("  Execution data file: ${it.absolutePath}, Exists: ${it.exists()}, Readable: ${it.canRead()}, Size: ${it.length()}")
            }
            println("Class directories used: ")
            classDirectories.asFileTree.forEach {
                println("  Class dir file: ${it.absolutePath}")
            }
            println("If report is empty, ensure classDirectories include actual .class files of your app, not just directories.")
        }
    }
}

I execute the jacoco task using this command

./gradlew jacocoTestReport --rerun-tasks -i

There are a lot of outputs but I believe this part is interesting:

> Task :app:createDebugAndroidTestCoverageReport
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/java', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/main/kotlin', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/kotlin', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/java', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/java', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/main/kotlin', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/kotlin', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/java', not found
Caching disabled for task ':app:createDebugAndroidTestCoverageReport' because:
  Build cache is disabled
  Caching has been disabled for the task
Task ':app:createDebugAndroidTestCoverageReport' is not up-to-date because:
  Executed with '--rerun-tasks'.
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/java', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/main/kotlin', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/kotlin', not found
file or directory '/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/src/debug/java', not found
View coverage report at file:/home/muhammad-sarim-mehdi/AndroidStudioProjects/ComposeShapeFitterSampleApp/app/build/reports/coverage/androidTest/debug/connected/index.html
Resolve mutations for :app:createDebugCoverageReport (Thread[#8032,Execution worker Thread 3,5,main]) started.
:app:createDebugCoverageReport (Thread[#8032,Execution worker Thread 3,5,main]) started.

> Task :app:createDebugCoverageReport
Skipping task ':app:createDebugCoverageReport' as it has no actions.
Resolve mutations for :app:jacocoTestReport (Thread[#8032,Execution worker Thread 3,5,main]) started.
:app:jacocoTestReport (Thread[#8032,Execution worker Thread 3,5,main]) started.

> Task :app:jacocoTestReport SKIPPED
Skipping task ':app:jacocoTestReport' as task onlyIf 'Any of the execution data files exists' is false.
AAPT2 aapt2-8.12.0-13700139-linux Daemon #0: shutdown
Build 1ff58828-508d-4e63-aa09-5b5f6f84abb4 is closed

I can see the coverage report due to the tests inside my androidTest directory but not due to the tests inside the test directory. What is wrong in my setup?

You only set the execution data if the execution data file exists at the time the configuration of the task is executed, so most probably that file does not exist yet, so you don’t configure it and then the task is skipped as there is not existing execution data file configured.

I found the solution. Here is the change I made:

tasks.register<JacocoReport>("jacocoTestReport") {
    // ...
    val kotlinClassesDirProvider = layout.buildDirectory.dir("tmp/kotlin-classes/debug")
    classDirectories.from(kotlinClassesDirProvider.map { dir ->
        project.fileTree(dir) {
            exclude(fileFilter)
        }
    })
    executionData.setFrom(
        fileTree(layout.buildDirectory.dir("outputs/unit_test_code_coverage/debugUnitTest")) {
            include("*.exec")
        }
    )
}

Now, jacoco was able to find the execution data

1 Like