Jacoco setup for android gradle and jenkins
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.gradle
file. 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
.
apply plugin: 'jacoco'
, if we apply the plugin here it automatically gets applied to the build.gradle
we 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 **
jacoco {
toolVersion = "0.7.2.201409121644"
}
jacoco
tasks to run after after the project is evaluated
.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!
}
collect
all the buildTypes
and productFlavors
into a List<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
foreach
loop that loops through our productFlavors
and buildTypes
. And since our source paths follow the same pattern we can define these at once as well. so we get freeDebug
and free/debug
on 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}"
}
}
}
build variant
we 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 named testFreeDebugUnitTestCoverage
in our first iteration. This is also the name you need to excecute to generate a test coverage report for this build variant gradle testFreeDebugUnitTestCoverage
, to check what gradle tasks are available for your project execute gradle 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.
group
and description
of the taskgroup = "Reporting"
description =
"Generate Jacoco coverage reports on the ${sourceName.capitalize()} build."
class directories
(Source sets that coverage should be reported for.) For our freeDebug build variant
this would be app/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
sourceDirectories = files(["src/main/java",
"src/$productFlavorName/java",
"src/$buildTypeName/java"])
.exec
files 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
reports {
xml.enabled = true
html.enabled = true
}
In order to get the coverage report we still have to add the jacoco.gradle
script to our app/build.gradle
. We can add gradle scripts as follows apply from: rootProject.file('gradle/jacoco.gradle')
(/gradle/jacoco.gradle)
Now that we added the script when we run the tasks command gradle tasks --all
you should find the Reporting Group
we 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.
.exec
files, which get created during building (since the jacoco plugin is applied to the build.gradle
script. DUH --> gradle build
or ./gradlew build
if your using the gradle wrapper..exec
files are created (/app/build/jacoco), you can run the gradle tasks to create the reportsgradle testDemoDebugUnitTestCoverage
(gradle supports multiple commands at once so you can run all at once, just seperate the tasks with a whitespace./app/build/reports/jacoco/testDemoDebugUnitTestCoverage/
and the html version should looks something like this..exec
files. Since They're now included in our .exec
, Jenkins will also show the correct test result.android {
testOptions {
unitTests.all {
jacoco {
includeNoLocationClasses = true
}
}
}
}
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:
app/build/jacoco/*.exec
app/build/intermediates/classes/free/**/be/vergauwen/simon/androidgradlejacoco,
app/build/intermediates/classes/paid/**/be/vergauwen/simon/androidgradlejacoco
app/src/main/java, app/src/paid/java, app/src/free/java
**/R.class,**/R$*.class,**/*$ViewInjector*.*,**/*$ViewBinder*.*,**/BuildConfig.*,**/Manifest*.*,**/*$Lambda$*.*,**/*Module.*,**/*Dagger*.*,**/*MembersInjector*.*,**/*_Provide*Factory*.*,**/*_Factory*.*,**/*$*$*.*
And hopefully you'll have a result as the following:
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.
gradle tasks
in order to create the reports, opposed to just building the project and having jenkins jacoco plugin
generate the reports.invoke gradle script
would look something like this:clean
build
testfreeDebugUnitTestCoverage
testpaidDebugUnitTestCoverage
testfreeReleaseUnitTestCoverage
testpaidReleaseUnitTestCoverage
HTML Publisher plugin
. Beware, you have to specify a new report for every build variant report.
jacoco plugin
.testOptions
for 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