maandag 18 augustus 2014

Code coverage reports using Robolectric and Android

Introduction


I've been writing quite a lot about Test Driven Development and some of the pitfalls that are complementing it. One of the import aspects of Test Driven Development, or Testing in General, is that you know what part of your code has been tested, and which still needs to be tested.

One of the frameworks you can use is JaCoCo, which integrates quite nicely with Gradle and Robolectric.


Update your build.gradle


First step is to update your build.gradle file. The excerpt can be found below. The full - working - example can be found on GitHub.

build.gradle

...

android {
    ...
    buildTypes {
        debug {
            runProguard false
            proguardFile 'proguard-rules.txt'
            debuggable true
            testCoverageEnabled = true

        }
    }

    ...
}

...

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.1.201405082137"
}

def coverageSourceDirs = [
        '../app/src/main/java'
]

task jacocoTestReport(type:JacocoReport, dependsOn: "testDebug") {
    group = "Reporting"

    description = "Generate Jacoco coverage reports"

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

    additionalSourceDirs = files(coverageSourceDirs)
    sourceDirectories = files(coverageSourceDirs)
    executionData = files('../app/build/jacoco/testDebug.exec')

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

}

Let's have a look at some of the most important elements.

  • You need to enable code coverage in your build type:
...

android {
    ...
    buildTypes {
        debug {
            runProguard false
            proguardFile 'proguard-rules.txt'
            debuggable true
            testCoverageEnabled = true

        }
    }

    ...
}
...
  • A new plugin is added and configured to the latest build version:
...

apply plugin: 'jacoco'

jacoco {
    toolVersion = "0.7.1.201405082137"
}

def coverageSourceDirs = [
        '../app/src/main/java'
]
...
  • In coverageSourceDirs you configure which folders are subject to introspection by JaCoCo. 
  • The JaCoCo plugin is configured to include your debug classes (which have been compiled) and to exclude the classes that you'll not test (e.g. the ButterKnife injections *$ViewInjector* classes).

...
task jacocoTestReport(type:JacocoReport, dependsOn: "testDebug") {
    group = "Reporting"

    description = "Generate Jacoco coverage reports"

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

    additionalSourceDirs = files(coverageSourceDirs)
    sourceDirectories = files(coverageSourceDirs)
    executionData = files('../app/build/jacoco/testDebug.exec')

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

}

Execute the gradle tasks

When you updated your gradle.build file, you will need to synchronise your development environment. Check if Gradle doesn't complain about any of the added plugins.

Before you can start to use the JaCoCo reports, you will need to provision the testDebug.exec file. The simplest way is to start a terminal window and executed the following gradle command on your project:

$ ./gradlew clean assemble


This will clear the compiled classes and assemble the build again.

Now you can render your JaCoCo reports via this command:

$ ./gradlew jacocoTestReport

The terminal will start your gradle build script, and one of the last tasks should be the rendering of the JaCoCo reports:
$ ./gradlew jacocoTestReport
:app:preBuild                
:app:preDebugBuild                
:app:checkDebugManifest                
:app:preReleaseBuild                 
:app:prepareComAndroidSupportSupportV42000Library UP-TO-DATE      
:app:prepareDeKeyboardsurferAndroidWidgetCrouton184Library UP-TO-DATE      
:app:prepareDebugDependencies                 
:app:compileDebugAidl UP-TO-DATE      
:app:compileDebugRenderscript UP-TO-DATE      
:app:generateDebugBuildConfig UP-TO-DATE      
:app:generateDebugAssets UP-TO-DATE      
:app:mergeDebugAssets UP-TO-DATE      
:app:generateDebugResValues UP-TO-DATE      
:app:generateDebugResources UP-TO-DATE      
:app:mergeDebugResources UP-TO-DATE      
:app:processDebugManifest UP-TO-DATE      
:app:processDebugResources UP-TO-DATE      
:app:generateDebugSources UP-TO-DATE      
:app:compileDebugJava UP-TO-DATE      
:app:compileTestDebugJava                                                                    
:app:processTestDebugResources UP-TO-DATE      
:app:testDebugClasses                 
:app:testDebug                                                             
:app:jacocoTestReport                                                           
               
BUILD SUCCESSFUL
               
Total time: 29.482 secs

The code coverage report will be stored in ./build/reports/jacoco/jacocoTestReport and might look alike the image below.


Some afterthoughts

  • The name of your Android application:
    • In our example, the name of the Android module is "app", ergo we include the sources from '../app/src/main/java'. If your Android app is called differently (e.g. FooBar), rename the paths (ALL OF THEM), accordingly, e.g. "../FooBar/src/main/java"
  • Product flavours
    • In the example we don't use product flavours, so the task depends on "testDebug" and you include classes from "../app/build/intermediates/classes/debug". If you would use product flavours in your android application (e.g. Local), Gradle will not find the "testDebug" task. You need to use the correct naming, e.g. testLocalDebug and include the correct classes, e.g. "../app/build/intermediates/classes/local/debug"


If you have any questions, please don't hesitate to ask. The code on GitHub has been updated accordingly, so please check it out.