⬆️ ⬇️

Measuring code coverage tests on Android using JaCoCo

Posted by: Mike Gouline

https://blog.gouline.net/2015/06/23/code-coverage-on-android-with-jacoco/

Translation: Semen Soldatenko



Since this feature appeared in the Android Gradle plugin version 0.10.0, many articles have been written about measuring code coverage by tests (test coverage) - and I have no illusions about this. However, what annoys me is the need to look at a few such articles and even the Gradle documentation before you get a fully working solution. So here, another article that will try to fix it and save your time.



Formulation of the problem



There is an Android project with unit tests (unit tests), and we want to create a report on code coverage for the tests performed. The solution should support different modes of build (build types) and product variations (product flavors).



Decision



The solution consists of several parts, so let's take it step by step.

')

Enable code coverage data collection.



You need to include support for collecting data on code coverage by tests for the build mode in which you will run the tests. Your build.gradle should contain the following:



 android { ... buildTypes { debug { testCoverageEnabled = true } ... } ... } 


Configure JaCoCo



Although all of this section could be placed in build.gradle , such a “mounted installation” will make your build script unreadable, so I recommend putting all this into a separate build script, and then importing.



We will begin setting up JaCoCo by creating a jacoco.gradle file in the project root directory. You can create it anywhere you want, but keeping it in the project root directory will make it easy to refer to it from all subprojects.



The easiest part is importing JaCoCo:



 apply plugin: 'jacoco' jacoco { toolVersion = "0.7.5.201505241946" } 


Please note that you do not need to declare any dependencies in order to use the “jacoco” plugin - all you need is to connect the Android plugin.



To check which version is the latest, look for org.jacoco: org.jacoco.core in jCenter, but update carefully - the latest version may still be incompatible, which can lead to some oddities, such as a blank report.



The next step is to create Gradle tasks for all product variations and build modes (in fact, you will only test the debug build (debug), but it’s very convenient to have this option for any special debug build configuration).



 def buildTypes = android.buildTypes.collect { type -> type.name } def productFlavors = android.productFlavors.collect { flavor -> flavor.name } 


Notice that the collect in Groovy takes a list as input, calls the function with each item in the list, and returns the results in a new list. In this case, the input receives lists of objects “assembly mode” and “product variation”, which are converted into lists of their names.



For projects in which product variations are not specified, we will add a blank name:



 if (!productFlavors) productFlavors.add('') 


Now we can scroll through them like this, which is essentially a nested loop in Groovy:



 productFlavors.each { productFlavorName -> buildTypes.each { buildTypeName -> ... } } 


The most important part is what we put inside the loop, so let's look at it in more detail.



First, we will prepare the task names with the correct placement of capital letters:



Here is how we define them:



 def sourceName, sourcePath if (!productFlavorName) { sourceName = sourcePath = "${buildTypeName}" } else { sourceName = "${productFlavorName}${buildTypeName.capitalize()}" sourcePath = "${productFlavorName}/${buildTypeName}" } def testTaskName = "test${sourceName.capitalize()}UnitTest" 


Now the challenge is how it actually looks like:



 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*.*', '**/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") reports { xml.enabled = true html.enabled = true } } 


You might see similar code in other JaCoCo articles, so I hope that most of it is understandable without explanation.



Parts worthy of attention:



That's all about jacoco.gradle , so here is the full contents of the file:



 apply plugin: 'jacoco' jacoco { toolVersion = "0.7.5.201505241946" } 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 } // 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" // 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") reports { xml.enabled = true html.enabled = true } } } } } 


Finally, you need to import this build script into your script in the app something like this:



 apply from: '../jacoco.gradle' 


(Note: This means that jacoco.gradle is located in the root directory of your project, as described above.)



That's all! You can ensure that tasks are created by running gradle tasks and looking for something like the following in the "Reporting" section:



 Reporting tasks --------------- testBlueDebugUnitTestCoverage - Generate Jacoco coverage reports on the BlueDebug build. testBlueReleaseUnitTestCoverage - Generate Jacoco coverage reports on the BlueRelease build. testRedDebugUnitTestCoverage - Generate Jacoco coverage reports on the RedDebug build. testRedReleaseUnitTestCoverage - Generate Jacoco coverage reports on the RedRelease build. 


To create a report, run gradle testBlueDebugUnitTestCoverage and you will find it in "build/reports/jacoco/testBlueDebugUnitTestCoverage/" .



Updates





Source



JaCoCo example (GitHub)

Source: https://habr.com/ru/post/280374/



All Articles