Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save prokash-sarkar/860f28f1f0822238e49cf22c4c767958 to your computer and use it in GitHub Desktop.
Save prokash-sarkar/860f28f1f0822238e49cf22c4c767958 to your computer and use it in GitHub Desktop.
Multi-Module, Multi-Flavored Test Coverage with JaCoCo in Android. Read the full article here: https://prokash-sarkar.medium.com/multi-module-multi-flavored-test-coverage-with-jacoco-in-android-bc4fb4d135a3
// App Level
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'jacoco'
apply from: "$project.rootDir/tools/script-jacoco.gradle"
android {
// Code omitted for brevity
flavorDimensions "version"
productFlavors {
app {
dimension "version"
}
beta {
dimension "version"
applicationIdSuffix ".beta"
}
prod {
dimension "version"
applicationIdSuffix ".prod"
}
}
dependencies {
// Code omitted for brevity
}
}
// Data Level
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'jacoco'
apply from: "$project.rootDir/tools/script-jacoco.gradle"
android {
// Code omitted for brevity
}
dependencies {
// Code omitted for brevity
}
// Domain Level
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply from: "$project.rootDir/tools/script-jacoco.gradle"
dependencies {
// Code omitted for brevity
}
// Code omitted for brevity
// Root Level
buildscript {
ext.kotlin_version = '1.4.21'
ext.jacoco_version = '0.8.6'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jacoco:org.jacoco.core:$jacoco_version"
}
}
/**
* How to run?
*
* App Module:
* ./gradlew app:testAppDebugUnitTestCoverage
* ./gradlew app:testBetaDebugUnitTestCoverage
* ./gradlew app:testProdDebugUnitTestCoverage
*
* Domain Module:
* ./gradlew domain:jacocoTestReport
*
* Data Module:
* ./gradlew data:testDebugUnitTestCoverage
*
* Where to find the reports?
*
* project/module/build/coverage-report
*/
private static boolean isAndroidModule(Project project) {
boolean isAndroidLibrary = project.plugins.hasPlugin('com.android.library')
boolean isAndroidApp = project.plugins.hasPlugin('com.android.application')
return isAndroidLibrary || isAndroidApp
}
afterEvaluate { project ->
def ignoreList = jacocoIgnoreList
def projectName = project.name
if (isAndroidModule(project)) setupAndroidReporting()
else setupKotlinReporting()
}
def setupKotlinReporting() {
jacocoTestReport {
dependsOn test
reports {
csv.enabled false // change if needed
xml.enabled false // change if needed
html {
enabled true
destination file("${buildDir}/coverage-report")
}
}
afterEvaluate {
classDirectories.from = files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
// dagger
'**/*_MembersInjector.class',
'**/Dagger*Component.class',
'**/Dagger*Component$Builder.class',
'**/Dagger*Subcomponent*.class',
'**/*Subcomponent$Builder.class',
'**/*Module_*Factory.class',
'**/di/module/*',
'**/*_Factory*.*',
'**/*Module*.*',
'**/*Dagger*.*',
'**/*Hilt*.*',
])
})
}
}
}
def setupAndroidReporting() {
tasks.withType(Test) {
// Whether or not classes without source location should be instrumented
jacoco.includeNoLocationClasses true
}
// 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"
System.out.println("Task -> $testTaskName")
// 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."
def fileFilter = [
// data binding
'android/databinding/**/*.class',
'**/android/databinding/*Binding.class',
'**/android/databinding/*',
'**/androidx/databinding/*',
'**/BR.*',
// android
'**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*',
'**/*Test*.*',
'android/**/*.*',
// dagger
'**/*_MembersInjector.class',
'**/Dagger*Component.class',
'**/Dagger*Component$Builder.class',
'**/Dagger*Subcomponent*.class',
'**/*Subcomponent$Builder.class',
'**/*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*.*',
// sealed and data classes
'**/*$Result.*',
'**/*$Result$*.*',
// adapters generated by moshi
'**/*JsonAdapter.*',
]
def javaTree = fileTree(dir: "${project.buildDir}/intermediates/javac/$sourceName/classes", excludes: fileFilter)
def kotlinTree = fileTree(dir: "${project.buildDir}/tmp/kotlin-classes/$sourceName", excludes: fileFilter)
classDirectories.from = files([javaTree], [kotlinTree])
executionData.from = files("${project.buildDir}/jacoco/${testTaskName}.exec")
def coverageSourceDirs = ["src/main/java",
"src/$productFlavorName/java",
"src/$buildTypeName/java"]
sourceDirectories.setFrom(files(coverageSourceDirs))
additionalSourceDirs.setFrom(files(coverageSourceDirs))
reports {
csv.enabled false // change if needed
xml.enabled false // change if needed
html {
enabled true
destination file("${buildDir}/coverage-report")
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment