Jacoco and Unit Tests Code Coverage with android-gradle-plugin >= 1.1

24,235

Solution 1

After the hassle, I decided to create an open source Gradle plugin for that.

Root build.gradle

buildscript {
    repositories {
        mavenCentral() // optional if you have this one already
    }
    dependencies {
        classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.16.0'
    }
}

apply plugin: 'com.vanniktech.android.junit.jacoco'

Then simply execute

./gradlew jacocoTestReportDebug

It'll run the JUnit tests in Debug Mode and then give you the Jacoco output in XML and HTML form in the corresponding build directory.

It also supports flavors. Having 2 flavors red and blue those tasks would be created

  • jacocoTestReportRedDebug
  • jacocoTestReportBlueDebug
  • jacocoTestReportRedRelease
  • jacocoTestReportBlueRelease

Solution 2

After some additional search I've stumbled upon this project I had to make some modifications so that there solution can work for my type of project, but now the test coverage reports are generated properly.

I've pushed the adopted changes to my example github repo in case someone will have a similar problem in the future.

Solution 3

I setup my unit tests for gradle 1.2 using this blog post. Then I pieced together information I found here and elsewhere to add code coverage to independent modules instead of the whole project. In my library module build.gradle file, I added the following:

apply plugin: 'jacoco'

def jacocoExcludes = [
        'com/mylibrary/excludedpackage/**'
]

android {
    ...
}

android.libraryVariants.all { variant ->
    task("test${variant.name.capitalize()}WithCoverage", type: JacocoReport, dependsOn: "test${variant.name.capitalize()}") {
        group = 'verification'
        description = "Run unit test for the ${variant.name} build with Jacoco code coverage reports."

        classDirectories = fileTree(
                dir: variant.javaCompile.destinationDir,
                excludes: rootProject.ext.jacocoExcludes.plus(jacocoExcludes)
        )
        sourceDirectories = files(variant.javaCompile.source)
        executionData = files("${buildDir}/jacoco/test${variant.name.capitalize()}.exec")

        reports {
            xml.enabled true
            xml.destination "${buildDir}/reports/jacoco/${variant.name}/${variant.name}.xml"
            html.destination "${buildDir}/reports/jacoco/${variant.name}/html"
        }
    }
}

And in my project build.gradle file, I added common excludes:

ext.jacocoExcludes = [
    'android/**',
    '**/*$$*',
    '**/R.class',
    '**/R$*.class',
    '**/BuildConfig.*',
    '**/Manifest*.*',
    '**/*Service.*'
]

Also, it looks like code coverage for unit tests may be coming built in in the future Issue 144664

Solution 4

Warning: This is a hack! Using your configuration above, I put together a hack to switch the android plugin between application and library depending on the build tasks chosen. This works well for me because I don't end up committing code with the application mode set.

// dynamically change the android plugin to application if we are running unit tests or test reports.
project.ext.androidPlugin = 'com.android.library'
for (String taskName : project.gradle.startParameter.taskNames) {
    if (taskName.contains('UnitTest') || taskName.contains('jacocoTestReport')) {
        project.ext.androidPlugin = 'com.android.application'
        break
    }
}

logger.lifecycle("Setting android pluging to ${project.ext.androidPlugin}")
apply plugin: project.ext.androidPlugin

...

apply plugin: 'jacoco'

configurations {
    jacocoReport
}

task jacocoTestReport(type:JacocoReport, dependsOn: "testDebug") {
    group = "Reporting"
    description = "Generate Jacoco coverage reports"

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

    sourceDirectories = files("${buildDir.parent}/src/main/java")
    additionalSourceDirs = files([
            "${buildDir}/generated/source/buildConfig/debug",
            "${buildDir}/generated/source/r/debug"
    ])
    executionData = files("${buildDir}/jacoco/testDebug.exec")

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

Let's hope the android tools team fixes this soon.

Solution 5

I was finally able to see my code coverage of JUnit tests with Android Studio 1.1.

jacoco.gradle

apply plugin: 'jacoco'

jacoco {
    toolVersion "0.7.1.201405082137"
}

def coverageSourceDirs = [
        "$projectDir/src/main/java",
]

task jacocoTestReport(type: JacocoReport, dependsOn: "testDebug") {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled = true
        html.enabled = true
    }
    classDirectories = fileTree(
            dir: './build/intermediates/classes/debug',
            excludes: ['**/R*.class',
                       '**/*$InjectAdapter.class',
                       '**/*$ModuleAdapter.class',
                       '**/*$ViewInjector*.class'
            ]
    )
    sourceDirectories = files(coverageSourceDirs)
    executionData = files("$buildDir/jacoco/testDebug.exec")
    // Bit hacky but fixes https://code.google.com/p/android/issues/detail?id=69174.
    // We iterate through the compiled .class tree and rename $$ to $.
    doFirst {
        new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
            if (file.name.contains('$$')) {
                file.renameTo(file.path.replace('$$', '$'))
            }
        }
    }
}

and then within the build.gradle file of the module (I put it between android and dependencies):

apply from: '../jacoco.gradle'

Also in the defaultConfig block of android. I've added this (don't know if it is necessary, but I've got this from this blog):

android {
    defaultConfig {
        testHandleProfiling true
        testFunctionalTest true
    }
}

Enjoy.

Share:
24,235
Serj Lotutovici
Author by

Serj Lotutovici

Android Developer & Software Architecture Geek. Former co-organizer of GDG Hamburg Android. Reader and occasional writer. Love #java #kotlin #android #music #gaming #vaping

Updated on September 17, 2021

Comments

  • Serj Lotutovici
    Serj Lotutovici almost 3 years

    I recently started integrating android-gradle-plugin 1.1.0 in one of my projects. The project uses robolectric 2.4 to run unit tests.

    It's a multi module project with very complex dependencies (Some modules depend on other modules). Something like that:

    --> application-module (dependsOn: module1, module2, module-core)
        --> module1 (dependsOn: module-core)
        --> module2 (dependsOn: module-core)
        --> module-core (dependsOn: module3, module4)
            --> module3 (library dependencies)
            --> module4 (library dependencies)
    

    For a more cleared picture please see jacoco-example project.

    I tried to integrate JaCoCo to generate reports for the unit tests, but it seems to me that it runs only androidTests which are basically instrumentation tests.

    After some google'ing I've come across a few projects on GitHub and other articles, but they mainly are focused on previous versions of the android-gradle-plugin or are using other third party plugins like android-unit-test for example here.

    May be I've lost my ability to google. But can somebody point me in a direction where I can find some documentations regarding the new stuff in android gradle plugin and how to run the jacoco task only for unit tests?

    UPDATE

    Adopted the script from nenick's example:

    apply plugin: "jacoco"
    
    configurations {
        jacocoReport
    }
    
    task jacocoReport(dependsOn: 'testDebug') << {
        ant {
            taskdef(name:'jacocoreport',
                    classname: 'org.jacoco.ant.ReportTask',
                    classpath: configurations.jacocoReport.asPath)
    
            mkdir dir: "${buildDir}/test-coverage-report"
            mkdir dir: "${buildDir}/reports/jacoco/test/"
    
            jacocoreport {
                executiondata = files("${buildDir}/jacoco/testDebug.exec")
    
                structure(name: "${rootProject.name}") {
                    classfiles {
                        fileset (dir: "${buildDir}/intermediates/classes/debug") {
                            //exclude(name: '**/*_*.class')
                            exclude(name: '**/R.class')
                            exclude(name: '**/R$*.class')
                            exclude(name: '**/BuildConfig.class')
                        }
                    }
    
                    sourcefiles {
                        fileset dir: "src/main/java"
                        fileset dir: "${buildDir}/generated/source/buildConfig/debug"
                        fileset dir: "${buildDir}/generated/source/r/debug"
                    }
                }
    
                xml destfile: "${buildDir}/reports/jacoco/test/jacocoTestReport.xml"
                html destdir: "${buildDir}/test-coverage-report/"
            }
        }
    }
    
    dependencies {
        jacocoReport 'org.jacoco:org.jacoco.ant:0.7.2.201409121644'
    }
    

    After that the ./gradlew jacocoReport executes and generates the report, but it shows 0 (zero) test coverage, which is impossible because at least half of all classes are tested.

    UPDATE_2

    Tried out this example. Adding the next task to one of my gradle build files:

    task jacocoTestReport(type:JacocoReport, dependsOn: "testDebug") {
        group = "Reporting"
        description = "Generate Jacoco coverage reports"
    
        classDirectories = fileTree(
                dir: "${buildDir}/intermediates/classes/debug",
                excludes: ['**/R.class',
                           '**/R$*.class',
                           '**/*$ViewInjector*.*',
                           '**/BuildConfig.*',
                           '**/Manifest*.*']
        )
    
        sourceDirectories = files("${buildDir.parent}/src/main/java")
        additionalSourceDirs = files([
                "${buildDir}/generated/source/buildConfig/debug",
                "${buildDir}/generated/source/r/debug"
        ])
        executionData = files("${buildDir}/jacoco/testDebug.exec")
    
        reports {
            xml.enabled = true
            html.enabled = true
        }
    }
    

    Same issue, the reports are generated, but the code coverage is still zero.

    UPDATE_3

    It seams that the task from UPDATE_2 worked but only for the module with apply plugin: 'com.android.application' (The reports a generated correctly). But for modules that are android libraries (apply plugin: 'com.android.library') the reports show zero coverage, although the modules contain more tests then the application module.

    UPDATE_4

    Created a simple example project that demonstrates my issue. Currently if you run ./gradlew jacocoReport the report is generated, but no test coverage is displayed for the module projects. See this link

    Short note: When the tests were AndroidUnitTests (whiteout JUnit 4 and Robolectric) JaCoCo reports showed coverage for all the modules.

    Any ideas?