AndroidGradleJacoco
AndroidGradleJacoco copied to clipboard
Jacoco setup for android gradle and jenkins
Android Gradle Jacoco (Test coverage)
Setting up Jacoco
-
First of all we'll have to setup Jacoco in Gradle. In order to avoid creating an enormous gradle file, we'll create a gradle file separately for Jacoco. This can be than be copied to any project and be imported into any build.gradle you need it.
-
By default there is a gradle folder in the root of your android project. This folder contains the Gradle wrapper, a little bit more about this in the Robolectric setup below. Within this folder we'll create a
jacoco.gradlefile. Don't worry if you don't have a lot of experience with gradle or groovy, it will all be explained below. If you don't require the extra explanation just checkout the code in this repo,/gradle/jacoco.gradle.
jacoco.gradle
apply plugin: 'jacoco', if we apply the plugin here it automatically gets applied to thebuild.gradlewe import this script into. You don't have to worry about dependencies, the android gradle plugin takes care of this for us!
**With the release Gradle 2.13, this is no longer required. More info below **
- I like to specify what jacoco is used because there can be some weird side-effects with jacoco versions. The version shown here should work fine! But more about this in the Jenkins/Robolectric setup.
jacoco {
toolVersion = "0.7.2.201409121644"
}
- Now for the real gradle work!
- We want the
jacocotasks to run after after the project isevaluated.
Such a listener gets notified when the build file belonging to this project has been executed.
project.afterEvaluate {
// Where we'll do all our work!
}
- First we're going to
collectall thebuildTypesandproductFlavorsinto aList<T>. In case there were no product flavors defined, we'll add an empty string element to the list.
def buildTypes = android.buildTypes.collect { type -> type.name }
def productFlavors = android.productFlavors.collect { flavor -> flavor.name }
if (!productFlavors) productFlavors.add('')
In our example we used the default build types (debug & release), and we defined a product flavor free and paid
- So we end up with 4 different build variants in AS:
- freeDebug
- paidDebug
- freeRelease
- paidRelease
- Because AS defines them productFlavorBuildType we'll be doing the same. So we'll have double
foreachloop that loops through ourproductFlavorsandbuildTypes. And since our source paths follow the same pattern we can define these at once as well. so we getfreeDebugandfree/debugon our first iteration, we need to do some more stuff in this iteration beside just defining it's name and path so let's do that!
productFlavors.each { productFlavorName ->
buildTypes.each { buildTypeName ->
def sourceName, sourcePath
if (!productFlavorName) {
sourceName = sourcePath = "${buildTypeName}"
} else {
sourceName = "${productFlavorName}${buildTypeName.capitalize()}"
sourcePath = "${productFlavorName}/${buildTypeName}"
}
}
}
- For each
build variantwe want to create a gradle task that generates the jacoco coverage report. So let's define a name for the gradle task first. The result being a task namedtestFreeDebugUnitTestCoveragein our first iteration. This is also the name you need to excecute to generate a test coverage report for this build variantgradle testFreeDebugUnitTestCoverage, to check what gradle tasks are available for your project executegradle tasks --all.
def testTaskName = "test${sourceName.capitalize()}UnitTestCoverage"
//noinspection GroovyAssignabilityCheck
task "${testTaskName}"(type: JacocoReport, dependsOn: "$testTaskName") {
//task code
}
Use //noinspection GroovyAssignabilityCheck to get rid of the annoying lint warning, it's a common issue.
- So let's do some playing nice stuff first. Setting the
groupanddescriptionof the task
group = "Reporting"
description =
"Generate Jacoco coverage reports on the ${sourceName.capitalize()} build."
- Now finally for the Jacoco specific settings!! First let's set the
class directories(Source sets that coverage should be reported for.) For ourfreeDebug build variantthis would beapp/build/intermediates/classes/free/debug/be/vergauwen/simon/androidgradlejacoco. To get a correct coverage report we need to exclude a bunch of class files. For example the files generated by libraries (dagger,butterknife,...). The files android generates (R, Manifest, BuildConfig, ...). When using a library you trust the developer that it works correctly, so these should not be included in our test coverage reports.
//Directory where the compiled class files are
classDirectories =
fileTree(dir: "${project.buildDir}/intermediates/classes/${sourcePath}",
excludes: ['**/R.class',
'**/R$*.class',
'**/*$ViewInjector*.*',
**/*$ViewBinder*.*',
'**/BuildConfig.*',
'**/Manifest*.*',
'**/*$Lambda$*.*', // Jacoco can not handle several "$" in class name.
'**/*Module.*', // Modules for Dagger.
'**/*Dagger*.*', // Dagger auto-generated code.
'**/*MembersInjector*.*', // Dagger auto-generated code.
'**/*_Provide*Factory*.*',
'**/*_Factory.*', //Dagger auto-generated code
'**/*$*$*.*' // Anonymous classes generated by kotlin
])
In case you're still seeing a classes that should not be included in your coverage report, exclude them here
- Next we define the source directories
sourceDirectories = files(["src/main/java",
"src/$productFlavorName/java",
"src/$buildTypeName/java"])
- And as our last jacoco paramater we need to define where our jacoco generated
.execfiles are located.
executionData = files("${project.buildDir}/jacoco/${testTaskName}.exec")
It is important to see the difference here! Our gradle tasks will generate our coverage reports! And our apply plugin: 'jacoco' will generate the .exec files while building the project
- Finally we define in which format we want our coverage report to be in
reports {
xml.enabled = true
html.enabled = true
}
Getting the coverage report
-
In order to get the coverage report we still have to add the
jacoco.gradlescript to ourapp/build.gradle. We can add gradle scripts as followsapply from: rootProject.file('gradle/jacoco.gradle')(/gradle/jacoco.gradle) -
Now that we added the script when we run the tasks command
gradle tasks --allyou should find the ReportingGroupwe defined with the Coverage report tasks.
Reporting tasks
---------------
testfreeDebugUnitTestCoverage - Generate Jacoco coverage reports on the freeDebug build.
testpaidDebugUnitTestCoverage - Generate Jacoco coverage reports on the paidDebug build.
testfreeReleaseUnitTestCoverage - Generate Jacoco coverage reports on the freeRelease build.
testpaidReleaseUnitTestCoverage - Generate Jacoco coverage reports on the paidRelease build.
- We need to create the
.execfiles, which get created during building (since the jacoco plugin is applied to thebuild.gradlescript. DUH -->gradle buildor./gradlew buildif your using the gradle wrapper. - When the
.execfiles are created (/app/build/jacoco), you can run the gradle tasks to create the reports gradle testDemoDebugUnitTestCoverage(gradle supports multiple commands at once so you can run all at once, just seperate the tasks with a whitespace.- The report can now be found in
/app/build/reports/jacoco/testDemoDebugUnitTestCoverage/and the html version should looks something like this.
Robolectric & Jenkins
- As of 25 April 2016 Gradle 2.13 was released. With Gradle 2.13 we have some extra options, and we can use the latest version of Jacoco. And thus have no problems with Jenkins or Robolectric anymore. (Should be the same for Powermock).
- With the following snippet Robolectric test will be included in our test coverage
.execfiles. Since They're now included in our.exec, Jenkins will also show the correct test result.
android {
testOptions {
unitTests.all {
jacoco {
includeNoLocationClasses = true
}
}
}
}
- To setup Gradle 2.13 as wrapper, make the following change in
gradle/wrapper/gradle-wrapper.properties:
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-all.zip
Setting up the Jacoco plugin is very similar to the gradle tasks we wrote. Except for that looping through build variants we need to hardcore the paths.
-
For our example the values would look like this:
- Path to exec files:
app/build/jacoco/*.exec - Path to class directories:
pp/build/intermediates/classes/free/**/be/vergauwen/simon/androidgradlejacoco, pp/build/intermediates/classes/paid/**/be/vergauwen/simon/androidgradlejacoco- Path to source directories:
app/src/main/java, app/src/paid/java, app/src/free/java - Exclusions:
**/R.class,**/R$*.class,**/*$ViewInjector*.*,**/*$ViewBinder*.*,**/BuildConfig.*,**/Manifest*.*,**/*$Lambda$*.*,**/*Module.*,**/*Dagger*.*,**/*MembersInjector*.*,**/*_Provide*Factory*.*,**/*_Factory*.*,**/*$*$*.*
- Path to exec files:
-
And hopefully you'll have a result as the following:
Deprecated - Could be of use if future updates break your setup
- There are a lot of know issues with Robolectric and Jacoco.
- The above used jacoco version will give the correct test coverage when using robolectric for unit testing. AWESOME I'm done! Well if you're using Jenkins and using the jacoco plugin you'll see a totally different result, the Robolectric tests are not included! (ノಠ益ಠ)ノ彡┻━┻
- Why!? The jacoco plugin creates the coverage reports for you instead of gradle, and is using the latest version. In other words, our older jacoco version we defined in our gradle script has 0 influence on our jacoco plugin and thus the "fix" is not working for Jenkins.
- Luckily I found two solutions!
Powermock seems to have the same issues as Robolectric, but both solution explained here should solve that 2. But I have not confirmed it yet.
Publish HTML result
- In case you want to keep to above explained script, and thus not use a later version of Jacoco and gradle (currently) 2.13-rc1. You can just publish the HTML reports created by our gradle script.
- First of all it's important to know that you have to run the
gradle tasksin order to create the reports, opposed to just building the project and havingjenkins jacoco plugingenerate the reports. - So for our example the
invoke gradle scriptwould look something like this:
clean
build
testfreeDebugUnitTestCoverage
testpaidDebugUnitTestCoverage
testfreeReleaseUnitTestCoverage
testpaidReleaseUnitTestCoverage
- To publish the html report we'll be using
HTML Publisher plugin. Beware, you have to specify a new report for every build variant report. - All you have to do in order to publish the html report is specify the location of the report, and give it a name.
- Now a report will be added, beware only the latest report will be viewable on the project page! And does not provide a graph like the
jacoco plugin.
Latest version all the things!!
- When using gradle 2.13 and a jacoco version >= 0.7.6, we have some extra options!!
- We can specify the following
testOptionsfor our android build.
android {
testOptions {
unitTests.all {
jacoco {
includeNoLocationClasses = true
}
}
}
}
Beware you need gradle version 2.13 in order to be able to use this option, or update your gradle version to 2.13
When using the wrapper use ./gradlew, if you've updated you can use gradle
--> Change the distributionUrl in your gradle-wrapper.properties to https\://services.gradle.org/distributions-snapshots/gradle-2.13-20160228000026+0000-all.zip
- Since we fixed the Robolectric bug, Jenkins can now happily show the correct test coverage result with the jacoco plugin.
Sources
- https://docs.gradle.org/current/userguide/build_lifecycle.html
- https://github.com/dampcake/Robolectric-JaCoCo-Sample
- https://habrahabr.ru/company/rambler-co/blog/266837/
- https://github.com/jacoco/jacoco/pull/288
- https://github.com/appfoundry/android-project-template/blob/master/gradle/jacoco.gradle