JaCoCo 完整配置流程

JaCoCo 完整配置流程

本文档基于 JacocoDemo 项目(AGP 8.10.1 + Gradle Kotlin DSL + Java 11),详细说明如何在 Android 项目中配置 JaCoCo 生成代码覆盖率报告。


一、JaCoCo 是什么?

JaCoCo(Java Code Coverage)是一个开源的 Java 代码覆盖率工具。它通过字节码插桩(Bytecode Instrumentation)技术,在代码运行时收集执行信息,最终生成覆盖率报告。

1.1 覆盖率指标说明

JaCoCo 提供以下 5 种维度的覆盖率指标:

指标 英文 说明 计算方式
指令覆盖率 Instruction Coverage 每个字节码指令是否被执行 执行指令数 / 总指令数
行覆盖率 Line Coverage 每行代码是否被执行 执行行数 / 总行数
分支覆盖率 Branch Coverage if/else/switch 等分支是否全部覆盖 执行分支数 / 总分支数
圈复杂度 Cyclomatic Complexity 代码的复杂度度量 独立路径数量
类覆盖率 Class Coverage 每个类是否被加载 加载类数 / 总类数
方法覆盖率 Method Coverage 每个方法是否被调用 调用方法数 / 总方法数

1.2 各指标的关系

复制代码
指令覆盖率 ≥ 行覆盖率 ≥ 分支覆盖率
  • 行覆盖率是最常用的指标,但可能掩盖问题(一行有多个分支时)
  • 分支覆盖率更严格,要求 if/else 的每个分支都被执行到
  • 指令覆盖率最精确,但数值通常最高

1.3 覆盖率参考标准

覆盖率 评价
0% - 40% 低,测试覆盖严重不足
40% - 60% 一般,核心逻辑可能有覆盖
60% - 80% 良好,大部分代码有测试
80% - 100% 优秀,测试覆盖充分

二、在 app/build.gradle.kts 中应用 JaCoCo 插件

2.1 插件声明

kotlin 复制代码
plugins {
    alias(libs.plugins.android.application)
    id("jacoco")  // 应用 JaCoCo 插件
}

说明 :Android Gradle Plugin(AGP)已经内置了 JaCoCo 插件的依赖,所以直接使用 id("jacoco") 即可,无需指定版本号。


三、开启覆盖率采集

3.1 在 buildTypes 中启用

kotlin 复制代码
android {
    buildTypes {
        debug {
            // AGP 8.x 开启本地单元测试覆盖率采集
            enableUnitTestCoverage = true

            // 仪器化测试覆盖率(如需开启)
            // enableAndroidTestCoverage = true
        }
    }
}

3.2 两种覆盖率采集

配置项 说明 生成文件
enableUnitTestCoverage = true 本地单元测试覆盖率 .exec 文件在 outputs/unit_test_code_coverage/
enableAndroidTestCoverage = true 仪器化测试覆盖率 .exec 文件在 outputs/android_test_code_coverage/

本项目仅开启了本地单元测试覆盖率,因为本地测试运行更快,且足以覆盖大部分业务逻辑。


四、JDK11 适配配置

4.1 问题背景

JaCoCo 在 JDK11 环境下运行时,可能会遇到以下问题:

  • NoLocationClasses 异常 :JaCoCo 尝试插桩 JDK 内部类(如 jdk.internal)时失败
  • Android 资源不可用 :本地 JVM 不加载 Android 资源,导致 ResourcesNotFoundException

4.2 解决方案

本项目在 app/build.gradle.kts 中的完整配置:

kotlin 复制代码
android {
    // JDK11 适配 jacoco 关键配置,解决 NoLocationClasses 异常
    testOptions {
        // 允许单元测试访问 Android 资源
        unitTests.isIncludeAndroidResources = true

        // Android 框架方法返回默认值而非 null
        unitTests.isReturnDefaultValues = true

        // 排除 JDK 内部类,避免 JaCoCo 插桩失败
        unitTests.all {
            it.exclude("**/jdk/internal/**")
        }
    }
}

4.3 各配置项详解

配置项 作用 不配置的后果
isIncludeAndroidResources = true 将 Android 资源打包进测试 APK 测试中访问 R.string.xxx 会崩溃
isReturnDefaultValues = true Context.getSystemService() 等返回默认值 调用 Android API 返回 null 导致 NPE
exclude("**/jdk/internal/**") 排除 JDK 内部类 JaCoCo 插桩 JDK 内部类导致 NoLocationClasses 异常

五、JacocoReport Task 完整配置

5.1 完整配置代码

kotlin 复制代码
afterEvaluate {
    tasks.register("jacocoUnitReport", JacocoReport::class) {
        dependsOn("testDebugUnitTest") // 依赖单元测试执行完成

        reports {
            html.required.set(true)  // 开启 HTML 报告(必看)
            xml.required.set(true)   // XML 用于 CI 集成
            csv.required.set(false)
        }

        // 源码路径
        sourceDirectories.setFrom(
            files("${projectDir}/src/main/java")
        )

        // class 字节码路径 + 过滤不需要统计的类
        classDirectories.setFrom(
            fileTree("${layout.buildDirectory.get()}/intermediates/javac/debug/compileDebugJavaWithJavac/classes") {
                exclude(
                    "**/R.class",
                    "**/R\$*.class",
                    "**/BuildConfig*.class",
                    "**/Manifest*.class",
                    "**/*Test*.class"
                )
            }
        )

        // jacoco 执行数据文件
        executionData.setFrom(
            file("${layout.buildDirectory.get()}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")
        )
    }
}

5.2 各配置项详解

5.2.1 dependsOn("testDebugUnitTest")
kotlin 复制代码
dependsOn("testDebugUnitTest")

作用:确保在执行 JaCoCo 报告生成之前,先完成单元测试的执行。

为什么需要

  • JaCoCo 报告需要 .exec 执行数据文件
  • .exec 文件由 testDebugUnitTest task 生成
  • 如果不设置依赖,可能因 .exec 文件不存在而报错

执行顺序

bash 复制代码
testDebugUnitTest → 生成 .exec 文件 → jacocoUnitReport → 生成 HTML/XML 报告
5.2.2 reports --- 报告格式
kotlin 复制代码
reports {
    html.required.set(true)   // HTML 报告
    xml.required.set(true)    // XML 报告
    csv.required.set(false)   // CSV 报告(关闭)
}
格式 用途 输出路径
HTML 人工查看,可视化高亮 build/reports/jacoco/jacocoUnitReport/html/
XML CI/CD 集成(SonarQube、Jenkins) build/reports/jacoco/jacocoUnitReport/xml/
CSV 数据分析(本项目关闭) build/reports/jacoco/jacocoUnitReport/csv/
5.2.3 sourceDirectories --- 源码路径
kotlin 复制代码
sourceDirectories.setFrom(
    files("${projectDir}/src/main/java")
)

作用:指定 Java 源码目录,JaCoCo 需要将覆盖率数据映射回源码行号。

输出效果:HTML 报告中可以直接查看带覆盖率高亮的源码。

5.2.4 classDirectories --- 字节码路径
kotlin 复制代码
classDirectories.setFrom(
    fileTree("${layout.buildDirectory.get()}/intermediates/javac/debug/compileDebugJavaWithJavac/classes") {
        exclude(
            "**/R.class",           // 资源类(自动生成)
            "**/R\$*.class",        // 资源内部类(如 R.string)
            "**/BuildConfig*.class",// 构建配置类(自动生成)
            "**/Manifest*.class",   // Manifest 类(自动生成)
            "**/*Test*.class"       // 测试类本身
        )
    }
)

AGP 8.x 字节码路径说明

AGP 版本 字节码路径
AGP 8.x intermediates/javac/debug/compileDebugJavaWithJavac/classes
AGP 7.x intermediates/classes/debug/
AGP 4.x intermediates/javac/debug/

注意:AGP 8.x 的路径结构与旧版本不同,如果从旧项目迁移,需要更新此路径。

排除规则说明

排除模式 原因
**/R.class 自动生成的资源索引类,无业务逻辑
**/R\$*.class R 类的内部类(如 R.string、R.layout)
**/BuildConfig*.class 自动生成的构建配置类
**/Manifest*.class 自动生成的 Manifest 类
**/*Test*.class 测试类本身不计入覆盖率统计
5.2.5 executionData --- 执行数据文件
kotlin 复制代码
executionData.setFrom(
    file("${layout.buildDirectory.get()}/outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")
)

.exec 文件说明

  • 二进制格式文件,包含代码执行信息
  • testDebugUnitTest task 在执行测试时自动生成
  • 只有开启了 enableUnitTestCoverage = true 才会生成

AGP 8.x 执行数据路径

AGP 版本 执行数据路径
AGP 8.x outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec
AGP 7.x outputs/unit_test_code_coverage/debug/testDebugUnitTest.exec

六、为什么使用 afterEvaluate

6.1 代码位置

kotlin 复制代码
afterEvaluate {
    tasks.register("jacocoUnitReport", JacocoReport::class) {
        // ...
    }
}

6.2 原因分析

afterEvaluate 的作用是在 Gradle 评估完整个 build.gradle.kts 脚本 之后 再执行其中的代码。

为什么 JaCoCo task 需要 afterEvaluate

  1. AGP 的 task 在脚本评估后期创建

    • testDebugUnitTest task 由 AGP 插件在脚本评估过程中动态创建
    • 如果在脚本顶层直接引用,可能找不到该 task
  2. layout.buildDirectory 的解析时机

    • 构建目录路径需要在 AGP 配置完成后才能确定
    • afterEvaluate 确保此时路径已正确解析
  3. 避免 Task not found 错误

    • 不使用 afterEvaluate 时,dependsOn("testDebugUnitTest") 可能报错

6.3 不使用 afterEvaluate 的替代方案

kotlin 复制代码
// 替代方案:使用 task 名称字符串延迟解析
tasks.register("jacocoUnitReport", JacocoReport::class) {
    // dependsOn 使用字符串,Gradle 会自动延迟解析
    dependsOn("testDebugUnitTest")
    // ...
}

但在本项目中,afterEvaluate 是最稳妥的方式,确保所有 AGP 配置都已生效。


七、完整配置汇总

7.1 app/build.gradle.kts 中与 JaCoCo 相关的全部配置

kotlin 复制代码
// 1. 应用插件
plugins {
    id("jacoco")
}

// 2. 开启覆盖率采集
android {
    buildTypes {
        debug {
            enableUnitTestCoverage = true
        }
    }

    // 3. JDK11 适配
    testOptions {
        unitTests.isIncludeAndroidResources = true
        unitTests.isReturnDefaultValues = true
        unitTests.all {
            it.exclude("**/jdk/internal/**")
        }
    }
}

// 4. 依赖声明
dependencies {
    testImplementation(libs.junit)
}

// 5. 报告生成 task
afterEvaluate {
    tasks.register("jacocoUnitReport", JacocoReport::class) {
        dependsOn("testDebugUnitTest")
        reports {
            html.required.set(true)
            xml.required.set(true)
            csv.required.set(false)
        }
        sourceDirectories.setFrom(files("${projectDir}/src/main/java"))
        classDirectories.setFrom(/* ... */)
        executionData.setFrom(/* ... */)
    }
}

7.2 配置流程图解

bash 复制代码
┌─────────────────────────────────────────────────┐
│  1. plugins { id("jacoco") }                    │
│     ↓ 应用 JaCoCo 插件                           │
│  2. enableUnitTestCoverage = true               │
│     ↓ 开启覆盖率采集开关                          │
│  3. testOptions { ... }                         │
│     ↓ JDK11 适配,排除内部类                      │
│  4. gradlew testDebugUnitTest                   │
│     ↓ 执行测试,生成 .exec 文件                   │
│  5. gradlew jacocoUnitReport                    │
│     ↓ 读取 .exec + 源码 + 字节码 → 生成报告       │
│  6. 查看 app/build/reports/jacoco/jacocoUnitReport/html/ │
│     ↓ 浏览器打开 index.html                      │
└─────────────────────────────────────────────────┘

八、常见问题排查

Q1:执行 jacocoUnitReport 报错 "Execution data does not exist"?

原因 :未先执行 testDebugUnitTest 或未开启覆盖率采集。

解决

bash 复制代码
# 先执行测试
gradlew testDebugUnitTest

# 再生成报告(jacocoUnitReport 已配置 dependsOn,直接执行也可)
gradlew jacocoUnitReport

Q2:HTML 报告中源码显示 "Source code not found"?

原因sourceDirectories 路径配置不正确。

解决 :确认路径为 ${projectDir}/src/main/javaprojectDir 指向 app/ 目录)。

Q3:覆盖率显示为 0%?

排查步骤

  1. 确认 enableUnitTestCoverage = true 已配置
  2. 确认 .exec 文件已生成
  3. 确认 classDirectories 路径与 AGP 版本匹配
  4. 确认测试类没有被 exclude 规则误排除

Q4:AGP 升级后 JaCoCo 报告生成失败?

原因:AGP 升级后编译产物路径可能变化。

解决:检查以下路径是否匹配当前 AGP 版本:

  • 字节码路径:intermediates/javac/debug/compileDebugJavaWithJavac/classes
  • 执行数据路径:outputs/unit_test_code_coverage/debugUnitTest/

九、总结

配置项 本项目实际值
JaCoCo 插件 id("jacoco")(AGP 内置,无需版本)
覆盖率采集 enableUnitTestCoverage = true
JDK11 适配 isIncludeAndroidResources = true + isReturnDefaultValues = true
JDK 内部类排除 exclude("**/jdk/internal/**")
报告格式 HTML + XML
字节码路径 intermediates/javac/debug/compileDebugJavaWithJavac/classes
执行数据路径 outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec
报告 Task jacocoUnitReport(包裹在 afterEvaluate 中)
相关推荐
QING6182 小时前
Android面试 —— 八股文之app启动流程
android·面试·app
海鸥-w2 小时前
python(fastapi) 实现更新,新增,删除接口
android·python·fastapi
le1616162 小时前
Android Compose Modifier修饰符
android·compose·modifier
黄林晴2 小时前
Android17新规:内存超限直接杀App,没有崩溃日志怎么排查?
android
Yeyu2 小时前
Binder 阻塞检测:跨进程通信的性能陷阱与监控方案
android·性能优化
●VON2 小时前
鸿蒙Flutter实战:日期选择器与截止日期高亮提醒
android·flutter·华为·harmonyos·鸿蒙
流星白龙3 小时前
【MySQL高阶】20.InnoDB 磁盘文件
android·mysql·adb
●VON3 小时前
鸿蒙Flutter实战:Material 3种子色亮暗双主题系统
android·flutter·harmonyos
灰鲸广告联盟3 小时前
新老用户广告价值不同?差异化策略如何实现收益最大化
android·开发语言·flutter·ios