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文件由testDebugUnitTesttask 生成- 如果不设置依赖,可能因
.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 文件说明:
- 二进制格式文件,包含代码执行信息
- 由
testDebugUnitTesttask 在执行测试时自动生成 - 只有开启了
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:
-
AGP 的 task 在脚本评估后期创建
testDebugUnitTesttask 由 AGP 插件在脚本评估过程中动态创建- 如果在脚本顶层直接引用,可能找不到该 task
-
layout.buildDirectory的解析时机- 构建目录路径需要在 AGP 配置完成后才能确定
afterEvaluate确保此时路径已正确解析
-
避免
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/java(projectDir 指向 app/ 目录)。
Q3:覆盖率显示为 0%?
排查步骤:
- 确认
enableUnitTestCoverage = true已配置 - 确认
.exec文件已生成 - 确认
classDirectories路径与 AGP 版本匹配 - 确认测试类没有被
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 中) |