JaCoCo doesn't work with Robolectric tests
Asked Answered
A

7

27

I wanted to generate code coverage reports on my JUnit tests in my android project so I added the JaCoCo gradle plugin. This is my project level build.gradle file:

apply plugin: 'jacoco'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.0.0-beta6'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

allprojects {
    repositories {
        jcenter()
        maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

subprojects { prj ->
    apply plugin: 'jacoco'

    jacoco {
        toolVersion '0.7.6.201602180812'
    }

    task jacocoReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {
        group = 'Reporting'
        description = 'Generate Jacoco coverage reports after running tests.'

        reports {
            xml {
                enabled = true
                destination "${prj.buildDir}/reports/jacoco/jacoco.xml"
            }
            html {
                enabled = true
                destination "${prj.buildDir}/reports/jacoco"
            }
        }

        classDirectories = fileTree(
                dir: 'build/intermediates/classes/debug',
                excludes: [
                        '**/R*.class',
                        '**/BuildConfig*',
                        '**/*$$*'
                ]
        )

        sourceDirectories = files('src/main/java')
        executionData = files('build/jacoco/testDebugUnitTest.exec')

        doFirst {
            files('build/intermediates/classes/debug').getFiles().each { file ->
                if (file.name.contains('$$')) {
                    file.renameTo(file.path.replace('$$', '$'))
                }
            }
        }
    }
}

jacoco {
    toolVersion '0.7.6.201602180812'
}

task jacocoFullReport(type: JacocoReport, group: 'Coverage reports') {
    group = 'Reporting'
    description = 'Generates an aggregate report from all subprojects'

    //noinspection GrUnresolvedAccess
    dependsOn(subprojects.jacocoReport)

    additionalSourceDirs = project.files(subprojects.jacocoReport.sourceDirectories)
    sourceDirectories = project.files(subprojects.jacocoReport.sourceDirectories)
    classDirectories = project.files(subprojects.jacocoReport.classDirectories)
    executionData = project.files(subprojects.jacocoReport.executionData)

    reports {
        xml {
            enabled = true
            destination "${buildDir}/reports/jacoco/full/jacoco.xml"
        }
        html {
            enabled = true
            destination "${buildDir}/reports/jacoco/full"
        }
    }

    doFirst {
        //noinspection GroovyAssignabilityCheck
        executionData = files(executionData.findAll { it.exists() })
    }
}

It works great by running ./gradlew jacocoFullReport. But unfortunately coverage is not reported for the tests that are run with the RobolectricTestRunner (instructions that are obviously called in the tests are not reported as covered). Tests with no @RunWith annotation or run with MockitoJUnitTestRunner report coverage just fine.

Any help would be appreciated to fix this problem.

Update 1: I noticed that I should be using the RobolectricGradleTestRunner. But it didn't help.

Acroterion answered 1/3, 2016 at 22:42 Comment(3)
I want to say that this is definitely possible, but I've no idea how to fix it. I managed to get code coverage working for Robolectric at an old job, but that was a year ago and I no longer have access to that code anymore. Sorry I can't be of more help!Matchmaker
testCoverageEnabled in later versions of gradle uses Jacoco, you dont need to apply plugin. Try without the plugin.Denaturalize
@NikolaDespotoski I believe setting testCoverageEnabled to true only works with android instrumentation tests, which require a connected device. And that's what I try to avoid by using Robolectric.Acroterion
H
25

It is known issue with the possible workaround - https://github.com/jacoco/jacoco/pull/288

Or downgrade jacoco version to 0.7.1.201405082137

UPDATE

The workaround is not needed anymore. You must use gradle version 2.13 and jacoco version 0.7.6.201602180812.

Update root build.gradle:

buildscript {
    dependencies {
        classpath 'org.jacoco:org.jacoco.core:0.7.6.201602180812'
    }
}

task wrapper( type: Wrapper ) {
  gradleVersion = '2.13'
}

Run ./gradlew wrapper

Update project build.gradle:

apply plugin: 'jacoco'

android {
  testOptions {
    unitTests.all {
      jacoco {
        includeNoLocationClasses = true
      }
    }
  }
}
Hoover answered 2/3, 2016 at 14:49 Comment(8)
Unfortunately the workaround doesn't seem to work for me.Acroterion
In this case if it is not principal for you then lower version of jacoco to 0.7.1.201405082137. It still might be the problem if you use jenkins latest jacoco plugin. Also downgrade version of it to something 1.8Hoover
Downgrading jacoco did the trick. I use travis-ci and there is no problem with it.Acroterion
In the update section, doing classpath 'org.jacoco:org.jacoco.core:0.7.6.201602180812' should not be required as that's what apply plugin does anyway.Selfsustaining
I'm not sure, but if you remove it than you will use default jacoco plugin and I'm not sure if it is latest. Did you try this?Hoover
Get the following error after following these step - Error:No such property: includeNoLocationClasses for class: org.gradle.testing.jacoco.plugins.JacocoTaskExtension_DecoratedDeandre
Which version of havoc do you use?Hoover
I have it working on Jacoco: toolVersion = "0.7.7.201606060606", what changed for me was just adding the includeNoLocationClasses = truePendentive
I
8

The accepted answer is a bit dated. Here is a similar fix we just implemented. In the module (i.e. app) build.gradle add:

apply plugin: 'jacoco'

tasks.withType(Test) {
    jacoco.includeNoLocationClasses = true
}

This does require JaCoCo 7.6+, but you are likely using it already.

Notes for Studio:

  1. This only fixes the CLI. If you run coverage from Studio using JaCoCo, the Robolectric coverage is still not reported. The default IntelliJ Coverage Runner seems to work fine.
  2. The test were crashing intermittently in Studio unless I added -noverify to the Android JUnit -> VM Options
Indulgent answered 21/6, 2018 at 21:59 Comment(1)
This seems to be the most accurate answer to date.Color
P
4

I was facing the same issue but now it is resolved for me by following this link,

issue link: https://github.com/robolectric/robolectric/issues/2230

Solution for this problem is mentioned here:

https://github.com/dampcake/Robolectric-JaCoCo-Sample/commit/f9884b96ba5e456cddb3d4d2df277065bb26f1d3

Pacesetter answered 3/3, 2016 at 10:44 Comment(1)
Eugen Martynov suggested the same thing. But it didn't work for me.Acroterion
S
4

This is an old issue, but for those who are still facing, it is worth mentioning that if you are setting up JaCoCo + Robolectric + Espresso - you will indeed be using includeNoLocationClasses, but still will see this error with Java9+ and probably end up here. Add the below snippet to your module build.gradle file

tasks.withType(Test) {
  jacoco.includeNoLocationClasses = true
  jacoco.excludes = ['jdk.internal.*']
}
Soda answered 11/4, 2022 at 21:25 Comment(1)
Thanks a lot for mentioning the jacoco.excludes statement. Without that the unit tests in my project crashed.Shrub
S
3

I had the same issue. I changed the jacoco plugin version and added includenolocationclasses property. Here is the working jacoco.gradle file (I am using gradle wrapper 2.14.1):

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.6.201602180812"
}

android {
    testOptions {
        unitTests.all {
            jacoco {
                includeNoLocationClasses = true
            }
        }
    }
}

project.afterEvaluate {
    // Grab all build types and product flavors
    def buildTypes = android.buildTypes.collect { type -> type.name }
    def productFlavors = android.productFlavors.collect { flavor -> flavor.name }
    println(buildTypes)
    println(productFlavors)
    // When no product flavors defined, use empty
    if (!productFlavors) productFlavors.add('')

    productFlavors.each { productFlavorName ->
        buildTypes.each { buildTypeName ->
            def sourceName, sourcePath
            if (!productFlavorName) {
                sourceName = sourcePath = "${buildTypeName}"
            } else {
                sourceName = "${productFlavorName}${buildTypeName.capitalize()}"
                sourcePath = "${productFlavorName}/${buildTypeName}"
            }
            def testTaskName = "test${sourceName.capitalize()}UnitTest"
            println("SourceName:${sourceName}")
            println("SourcePath:${sourcePath}")
            println("testTaskName:${testTaskName}")
            // Create coverage task of form 'testFlavorTypeCoverage' depending on 'testFlavorTypeUnitTest'
            task "${testTaskName}Coverage" (type:JacocoReport, dependsOn: "$testTaskName") {
                group = "Reporting"
                description = "Generate Jacoco coverage reports on the ${sourceName.capitalize()} build."

                classDirectories = fileTree(
                        dir: "${project.buildDir}/intermediates/classes/${sourcePath}",
                        excludes: ['**/R.class',
                                   '**/R$*.class',
                                   '**/*$ViewInjector*.*',
                                   '**/*$ViewBinder*.*',
                                   '**/BuildConfig.*',
                                   '**/Manifest*.*']
                )

                def coverageSourceDirs = [
                        "src/main/java",
                        "src/$productFlavorName/java",
                        "src/$buildTypeName/java"
                ]
                additionalSourceDirs = files(coverageSourceDirs)
                sourceDirectories = files(coverageSourceDirs)
                executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec")
                println("${project.buildDir}/jacoco/${testTaskName}.exec")
                reports {
                    xml.enabled = true
                    html.enabled = true
                }
            }
        }
    }
}
Scaramouch answered 12/5, 2017 at 6:13 Comment(0)
E
0

Same issue here, but the solutions provided no longer work with Gradle 8.7/AGP 8.4.1. For me, it was working with this piece of code in the module's build.gradle.kts file:

tasks.withType(Test::class) {
    configure<JacocoTaskExtension> {
        isIncludeNoLocationClasses = true
        excludes = listOf("jdk.internal.*")
    }
}

I have enabled the jacoco plugin in the corresponding section:

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

Then I setup code coverage instrumentation in the android section:

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

And finally I have added the Robolectric dependency:

dependencies {
    ...
    testImplementation("org.robolectric:robolectric:4.12.2")
}

For the full implementation, see here: https://github.com/Futsch1/medTimer/blob/main/app/build.gradle.kts

Erminia answered 18/6 at 19:0 Comment(1)
Can you provide some supporting information?Foetor
H
-1

change coverage runner to jacoco in android studio 1- select app(root of the project) 2 click on menu (run --> Edit configurations --> code coverage --> choose JaCoCo).

the screenshot is below

Hautboy answered 14/2, 2019 at 15:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.