In the previous story, we set up Git CI/CD + Fastlane for auto-deployment. We now going to go through how to set up the Jacoco plugin and integrate it in .gitlab-ci.yml
for the auto coverage test report.
Please have a look at the previous/next stories related to gitlab and CI for more information in case you haven’t yet.
Part 1:
Part 2:
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
From documentation:
JaCoCo should provide the standard technology for code coverage analysis in Java VM based environments. The focus is on providing a lightweight, flexible and well-documented library for integration with various build and development tools.
Let’s get started on how we can integrate JaCoCo in an android project and run the verification tests on CI.
Let’s add a plugin at the project level build.gradle
file.
buildscript {
repositories {
google()
...
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.0'
//Jacoco Plugin
classpath "org.jacoco:org.jacoco.core:0.8.7"
}
}
We’re going to create a separate Gradle file for Jacoco to avoid large configurations in the module’s build.gradle file.
Hers’s jacoco.gradle
file:
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.7"
}
project.afterEvaluate {
if (android.hasProperty("applicationVariants")) {
android.applicationVariants.all { variant ->
createVariantCoverage(variant)
}
} else if (android.hasProperty("libraryVariants")) {
android.libraryVariants.all { variant ->
createVariantCoverage(variant)
}
}
}
ext.excludes = [
'**/databinding/*Binding.*',
'**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*',
'**/*Test*.*',
'android/**/*.*',
// butterKnife
'**/*$ViewInjector*.*',
'**/*$ViewBinder*.*',
'**/Lambda$*.class',
'**/Lambda.class',
'**/*Lambda.class',
'**/*Lambda*.class',
'**/*_MembersInjector.class',
'**/Dagger*Component*.*',
'**/*Module_*Factory.class',
'**/di/module/*',
'**/*_Factory*.*',
'**/*Module*.*',
'**/*Dagger*.*',
'**/*Hilt*.*',
// kotlin
'**/*MapperImpl*.*',
'**/*$ViewInjector*.*',
'**/*$ViewBinder*.*',
'**/BuildConfig.*',
'**/*Component*.*',
'**/*BR*.*',
'**/Manifest*.*',
'**/*$Lambda$*.*',
'**/*Companion*.*',
'**/*Module*.*',
'**/*Dagger*.*',
'**/*Hilt*.*',
'**/*MembersInjector*.*',
'**/*_MembersInjector.class',
'**/*_Factory*.*',
'**/*_Provide*Factory*.*',
'**/*Extensions*.*'
]
def createVariantCoverage(variant) {
def variantName = variant.name
def testTaskName = "test${variantName.capitalize()}UnitTest"
// Add unit test coverage tasks
tasks.create(name: "${testTaskName}Coverage", type: JacocoReport, dependsOn: "$testTaskName") {
group = "Reporting"
description = "Generate Jacoco coverage reports for the ${variantName.capitalize()} build."
reports {
html.enabled = true
}
def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir, excludes: project.excludes)
def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: project.excludes)
getClassDirectories().setFrom(files([javaClasses, kotlinClasses]))
getSourceDirectories().setFrom(files([
"$project.projectDir/src/main/java",
"$project.projectDir/src/${variantName}/java",
"$project.projectDir/src/main/kotlin",
"$project.projectDir/src/${variantName}/kotlin"
]))
getExecutionData().setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/${variantName}UnitTest/${testTaskName}.exec"))
doLast {
def m = new File("${project.buildDir}/reports/jacoco/${testTaskName}Coverage/html/index.html")
.text =~ /Total[^%]*>(\d?\d?\d?%)/
if (m) {
println "Test coverage: ${m[0][1]}"
}
}
}
// Add unit test coverage verification tasks
tasks.create(name: "${testTaskName}CoverageVerification", type: JacocoCoverageVerification, dependsOn: "${testTaskName}Coverage") {
group = "Reporting"
description = "Verifies Jacoco coverage for the ${variantName.capitalize()} build."
violationRules {
rule {
limit {
minimum = 0
}
}
rule {
element = 'BUNDLE'
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.30
}
}
}
def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir, excludes: project.excludes)
def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: project.excludes)
getClassDirectories().setFrom(files([javaClasses, kotlinClasses]))
getSourceDirectories().setFrom(files([
"$project.projectDir/src/main/java",
"$project.projectDir/src/${variantName}/java",
"$project.projectDir/src/main/kotlin",
"$project.projectDir/src/${variantName}/kotlin"
]))
getExecutionData().setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/${variantName}UnitTest/${testTaskName}.exec"))
}
}
Let’s break it down
reports {
html.enabled = true
}
Specify the type of report we want to generate.
def javaClasses = fileTree(dir: variant.javaCompileProvider.get().destinationDir, excludes: project.excludes)
def kotlinClasses = fileTree(dir: "${buildDir}/tmp/kotlin-classes/${variantName}", excludes: project.excludes)
These two lines tell Jacoco about our kotlin and java classes.
excludes
contain a list of all classes and packages which we’re not going to cove in our test coverage.
getExecutionData().setFrom(files("${project.buildDir}/outputs/unit_test_code_coverage/${variantName}UnitTest/${testTaskName}.exec"))
Specify the path of .exec
file to find execution data.
You can find a detailed explanation of Jacoco properties HERE.
apply plugin: 'com.android.application'
...
apply from: '../jacoco.gradle'
Run ./gradlew testDevDebugUnitTestCoverageVerification
And, you will see generated report in the Jacoco directory.
//App build.gradle
buildTypes {
debug{
testCoverageEnabled true
}
}
To automate Jacoco converge verification, we’re going to add a job to run coverage verification on our behalf.
stages:
- coverageTest
- build
build-test:
stage: coverageTest
before_script:
- chmod +x ./gradlew
- export GRADLE_USER_HOME=$PWD/.gradle
script:
- ./gradlew app:testDevDebugUnitTestCoverageVerification
and that’s it. Push some code and Run your pipeline!