19

I have been trying to implement Jacoco for code coverage in Android kotlin project. I used deafult android studio coverage tool but it was not reliable. So I tried to implement Jacoco but I am getting 0% code coverage even after tests are passing successfully.

0% Coverage -

enter image description here

Succesfull test run -

enter image description here

Here is the gradle script -

plugins {
id 'com.android.library'
id 'kotlin-android'
id 'jacoco'
}


android {
compileSdkVersion 30
buildToolsVersion "30.0.3"

defaultConfig {
    minSdkVersion 21
    targetSdkVersion 30
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    consumerProguardFiles "consumer-rules.pro"
}

buildTypes {
    debug {
        debuggable true
        minifyEnabled false
        testCoverageEnabled true
    }
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
    jvmTarget = '1.8'
}
testOptions {
    unitTests.returnDefaultValues = true
}

}



dependencies {

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.3.0'
testImplementation 'junit:junit:4.+'
testImplementation 'org.mockito:mockito-core:3.11.2'

testImplementation 'org.mockito:mockito-inline:3.11.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

jacoco {
toolVersion = "0.8.5"
// Custom reports directory can be specfied like this:
// reportsDir = file("$buildDir/customJacocoReportDir")
}

tasks.withType(Test) {
jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*']
// see related issue https://github.com/gradle/gradle/issues/5184#issuecomment-457865951
}

project.afterEvaluate {

(android.hasProperty('applicationVariants')
        ? android.'applicationVariants'
        : android.'libraryVariants')
        .all { variant ->
            def variantName = variant.name
            def unitTestTask = "test${variantName.capitalize()}UnitTest"
            def androidTestCoverageTask = "create${variantName.capitalize()}CoverageReport"

            tasks.create(name: "${unitTestTask}Coverage", type: JacocoReport, dependsOn: [
                    "$unitTestTask",
                    "$androidTestCoverageTask"
            ]) {
                group = "Reporting"
                description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build"

                reports {
                    html.enabled = true
                    xml.enabled = true
                    csv.enabled = true
                }

                def excludes = [
                        // data binding
                        'android/databinding/**/*.class',
                        '**/android/databinding/*Binding.class',
                        '**/android/databinding/*',
                        '**/androidx/databinding/*',
                        '**/BR.*',
                        // android
                        '**/R.class',
                        '**/R$*.class',
                        '**/BuildConfig.*',
                        '**/Manifest*.*',
                        '**/*Test*.*',
                        'android/**/*.*',
                        // kotlin
                        '**/*MapperImpl*.*',
                        '**/*$ViewInjector*.*',
                        '**/*$ViewBinder*.*',
                        '**/BuildConfig.*',
                        '**/*Component*.*',
                        '**/*BR*.*',
                        '**/Manifest*.*',
                        '**/*$Lambda$*.*',
                        '**/*Companion*.*',
                        '**/*Module*.*',
                        '**/*Dagger*.*',
                        '**/*Hilt*.*',
                        '**/*MembersInjector*.*',
                        '**/*_MembersInjector.class',
                        '**/*_Factory*.*',
                        '**/*_Provide*Factory*.*',
                        '**/*Extensions*.*',
                        // sealed and data classes
                        '**/*$Result.*',
                        '**/*$Result$*.*'
                ]

                def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir,
                        excludes: excludes)
                def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}",
                        excludes: excludes)

                classDirectories.setFrom(files([
                        javaClasses,
                        kotlinClasses
                ]))

                def variantSourceSets = variant.sourceSets.java.srcDirs.collect { it.path }.flatten()
                sourceDirectories.setFrom(project.files(variantSourceSets))

                def androidTestsData = fileTree(dir: "${buildDir}/outputs/code_coverage/${variantName}AndroidTest/connected/", includes: ["**/*.ec"])

                executionData(files([
                        "$project.buildDir/jacoco/${unitTestTask}.exec",
                        androidTestsData
                ]))
            }

        }
}
riskPlayGround
  • 422
  • 5
  • 16

7 Answers7

7

Right now, for an android multimodule project with:

  • jacoco 0.8.7
  • gradle plugin 7.0.2

I am finding that if debug { testCoverageEnabled true } is set, jacoco produces no output for unit tests (eg testDebugUnitTest). Whereas if it is not, /build/jacoco/testDebugUnitTest.exec is created, with actual coverage data.

This is clearly a bug, but not sure what component it's in. It is bad because it means that it'll work for either device or unit tests, but not both at the same time.

Sofi Software LLC
  • 3,879
  • 1
  • 36
  • 34
3

For anyone who still facing the issue:

It's an Android Gradle Plugin issue

enter image description here

Upgrade Android Gradle Plugin to 7.2.1 will do the fix.

聂超群
  • 1,659
  • 6
  • 14
0

What I remember is that only a specific version of jacoco is supported:

apply plugin: 'jacoco'
jacoco {
    toolVersion = '0.7.5.201505241946'
}
Mahozad
  • 18,032
  • 13
  • 118
  • 133
Uriel Frankel
  • 14,304
  • 8
  • 47
  • 69
0

I have struggled with this issue for 3 years across multiple projects, but I have a solution (or rather someone who resolved my issues).

The Android-Root-Coverage-Plugin can be used to combine both jUnit and instrumented tests in a Java and/or Kotlin multi module project without any great need to configure anything else. It resolved my following issues:

  • Getting coverage in multi module project
  • Incorrect coverage in Kotlin
  • Combining both jUnit & Instrumented test coverage

It has been confirmed that the cause of my issues were due to a bug in the android gradle plugin and as of Aug, 2022 it has yet to be resolved.

LethalMaus
  • 798
  • 1
  • 8
  • 20
0

It seems like that are issues when using hilt + jacoco in the same project, so probably why we don't get coverage... https://github.com/google/dagger/issues/2740

Morteza Rastgoo
  • 6,772
  • 7
  • 40
  • 61
0

If you are using Hilt in your project, try adding build/intermediates/asm_instrumented_project_classes/<Product>Debug/ folder inside classDirectories in your Jacoco Task.

A related issue in issue tracker: https://issuetracker.google.com/issues/161300933#comment5

shubhamgarg1
  • 1,619
  • 1
  • 15
  • 20
-1

I assume you only have unit test and do not have any Android Instrument test??

If this is the case, after you run ./gradlew ${unitTestTask}Coverage. You should see the following file generated

enter image description here

Please absolutely avoid the coverage folder, that folder also contains a report, however, it is hard wired to only generate Android Instrument Test coverage.

If you click open the app/build/reports/jacoco folder

enter image description here

This xml report is the one generated from our jacoco gradle task. And It should contains both the Android Instrument test and unit test.

I enabled the html option from gradle task. And if I open the index.html in this jacoco folder. It is different result than the one generated from reports/coverage folder.

Also double check the unit test compile and run fine, otherwise it can get ignored and cause 0%

BabyishTank
  • 1,329
  • 3
  • 18
  • 39