Last active
January 24, 2026 04:26
-
-
Save mohitgupt/d1694f259816271e9a9cc18b1f197439 to your computer and use it in GitHub Desktop.
Android JaCoCo code for this tutorial: https://www.truiton.com/2026/01/android-jacoco-code-coverage-plugin-for-unit-testing/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Android CI | |
| on: | |
| push: | |
| branches: [ "master" ] | |
| pull_request: | |
| branches: [ "master" ] | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: set up JDK 17 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| cache: gradle | |
| - name: Grant execute permission for gradlew | |
| run: chmod +x gradlew | |
| - name: Generate JaCoCo report | |
| run: ./gradlew clean jacocoTestReport | |
| - name: Upload Report | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: report.xml | |
| path: ${{ github.workspace }}/**/build/reports/jacoco/**/jacocoTestReport.xml | |
| - name: JaCoCo Report | |
| id: jacoco | |
| uses: Madrapps/jacoco-report@v1.7.2 | |
| with: | |
| paths: | | |
| ${{ github.workspace }}/**/build/reports/jacoco/**/jacocoTestReport.xml, | |
| ${{ github.workspace }}/**/build/reports/jacoco/**/debugCoverage.xml | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| min-coverage-overall: 80 | |
| min-coverage-changed-files: 80 | |
| title: Code Coverage | |
| - name: Get the Coverage info | |
| run: | | |
| echo "Total coverage ${{ steps.jacoco.outputs.coverage-overall }}" | |
| echo "Changed Files coverage ${{ steps.jacoco.outputs.coverage-changed-files }}" | |
| - name: Fail PR if overall coverage is less than 80% | |
| if: ${{ steps.jacoco.outputs.coverage-overall < 80.0 }} | |
| uses: actions/github-script@v6 | |
| with: | |
| script: | | |
| core.setFailed('Overall coverage is less than 80%!') |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import com.google.firebase.perf.plugin.FirebasePerfExtension | |
| ... | |
| android { | |
| buildTypes { | |
| debug { | |
| ... | |
| configure<FirebasePerfExtension> { | |
| setInstrumentationEnabled(false) | |
| } | |
| } | |
| } | |
| } | |
| ... | |
| apply(from = "jacoco.gradle.kts") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import org.gradle.api.tasks.testing.Test | |
| import org.gradle.testing.jacoco.plugins.JacocoPluginExtension | |
| import org.gradle.testing.jacoco.plugins.JacocoTaskExtension | |
| import org.gradle.testing.jacoco.tasks.JacocoReport | |
| apply(plugin = "jacoco") | |
| configure<JacocoPluginExtension> { | |
| toolVersion = "0.8.12" | |
| } | |
| tasks.withType<Test> { | |
| configure<JacocoTaskExtension> { | |
| isIncludeNoLocationClasses = true | |
| excludes = listOf("jdk.internal.*") | |
| } | |
| } | |
| tasks.register<JacocoReport>("jacocoTestReport") { | |
| dependsOn("testDebugUnitTest") | |
| reports { | |
| xml.required.set(true) | |
| html.required.set(true) | |
| } | |
| val exclusions = listOf( | |
| "**/R.class", | |
| "**/R\$*.class", | |
| "**/BuildConfig.*", | |
| "**/Manifest*.*", | |
| "**/*Test*.*", | |
| "android/**/*.*", | |
| "**/Lambda\$*.class", | |
| "**/Lambda.class", | |
| "**/*Lambda.class", | |
| "**/*Lambda*.class", | |
| "**/*_MembersInjector.class", | |
| "**/Dagger*.*", | |
| "**/*Dagger*.*", | |
| "**/*_Factory.class", | |
| "**/*_Provide*Factory.class", | |
| "**/*_ViewBinding*.*", | |
| "**/BR.class", | |
| "**/DataBinderMapperImpl.class", | |
| "**/DataBinderMapperImpl\$*.class", | |
| "**/DataBindingInfo.class", | |
| "**/*\$Creator.class", | |
| "**/*\$DefaultImpls.class", | |
| "**/*\$Companion.class" | |
| ) | |
| sourceDirectories.setFrom(files("${project.projectDir}/src/main/java")) | |
| // Using specific paths for Java and Kotlin classes to avoid "different class with same name" error | |
| val javaClasses = fileTree("${layout.buildDirectory.get().asFile}/intermediates/javac/debug/classes") { | |
| exclude(exclusions) | |
| } | |
| val kotlinClasses = fileTree("${layout.buildDirectory.get().asFile}/tmp/kotlin-classes/debug") { | |
| exclude(exclusions) | |
| } | |
| classDirectories.setFrom(files(javaClasses, kotlinClasses)) | |
| executionData.setFrom(fileTree(layout.buildDirectory.get().asFile) { | |
| include(listOf("jacoco/testDebugUnitTest.exec", "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")) | |
| }) | |
| doLast { | |
| val reportFile = file("${layout.buildDirectory.get().asFile}/reports/jacoco/jacocoTestReport/jacocoTestReport.xml") | |
| if (reportFile.exists()) { | |
| val factory = javax.xml.parsers.DocumentBuilderFactory.newInstance() | |
| factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) | |
| val parser = factory.newDocumentBuilder() | |
| val doc = parser.parse(reportFile) | |
| val root = doc.documentElement | |
| val nodeList = root.childNodes | |
| println("-------------------------------------------------------") | |
| println("Code Coverage Summary (Project Level):") | |
| for (i in 0 until nodeList.length) { | |
| val node = nodeList.item(i) | |
| if (node is org.w3c.dom.Element && node.tagName == "counter") { | |
| val type = node.getAttribute("type") | |
| val missed = node.getAttribute("missed").toDouble() | |
| val covered = node.getAttribute("covered").toDouble() | |
| val total = missed + covered | |
| val percentage = if (total > 0) (covered / total) * 100 else 0.0 | |
| if (type == "INSTRUCTION") { | |
| println("Instructions : ${String.format("%.2f", percentage)}% (${covered.toInt()}/${total.toInt()})") | |
| } else if (type == "BRANCH") { | |
| println("Branches : ${String.format("%.2f", percentage)}% (${covered.toInt()}/${total.toInt()})") | |
| } else if (type == "LINE") { | |
| println("Lines : ${String.format("%.2f", percentage)}% (${covered.toInt()}/${total.toInt()})") | |
| } | |
| } | |
| } | |
| println("-------------------------------------------------------") | |
| } else { | |
| println("Jacoco report XML not found at ${reportFile.absolutePath}") | |
| } | |
| } | |
| } | |
| // Task to upload coverage and test reports to BrowserStack. | |
| tasks.register("uploadReportsToBrowserStack") { | |
| dependsOn("jacocoTestReport") | |
| doLast { | |
| val bsUser = if (project.hasProperty("bsUser")) project.property("bsUser") else "YOUR_ID" | |
| val bsKey = if (project.hasProperty("bsKey")) project.property("bsKey") else "YOUR_KEY" | |
| val buildId = java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm").format(java.util.Date()) | |
| // Upload JUnit XML Reports (to Automation Upload endpoint) | |
| val junitReportDir = file("${layout.buildDirectory.get().asFile}/test-results/testDebugUnitTest") | |
| if (junitReportDir.exists()) { | |
| println("Uploading JUnit reports to BrowserStack...") | |
| junitReportDir.listFiles { f -> f.extension == "xml" }?.forEach { xmlFile -> | |
| exec { | |
| commandLine( | |
| "curl", "-u", "$bsUser:$bsKey", "-vvv", | |
| "-X", "POST", | |
| "-F", "data=@${xmlFile.absolutePath}", | |
| "-F", "projectName=TambolaCaller", | |
| "-F", "buildName=Unit Test Coverage", | |
| "-F", "buildIdentifier=$buildId", | |
| "-F", "tags=unit_test, android", | |
| "-F", "frameworkVersion=junit, 4.13.2", | |
| "https://upload-automation.browserstack.com/upload" | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment