30、测试覆盖率与CI集成:JaCoCo配置、GitHub Actions集成、测试报告生成
说实话,很多团队写测试就是「为了写而写」。测了半天,到底覆盖了多少代码?没人知道。我以前也踩过这个坑——测试用例写了一堆,结果核心逻辑根本没测到。直到我把覆盖率工具和CI流水线串起来,才真正让测试变成了「质量守门员」。
这一章,我们就来聊聊怎么用 JaCoCo 做覆盖率分析,怎么把它集成到 GitHub Actions 里,以及怎么生成一份能看懂的报告。
为什么需要覆盖率?
覆盖率不是万能的,但没有覆盖率是万万不能的。它至少能告诉你三件事:
- 哪些代码从来没被执行过——可能是死代码,也可能是漏测了
- 测试的「盲区」在哪里——比如某个 if 分支永远没走到
- 代码变更后,测试有没有跟上——CI 里一跑就现原形
我个人习惯把覆盖率当作「健康检查」,而不是「KPI」。别为了凑 80% 去写一堆没意义的测试,那还不如不写。
JaCoCo 配置:从 Gradle 开始
JaCoCo 是 Java/Kotlin 生态里最常用的覆盖率工具。在 Compose 项目里配置它,其实很简单。
首先,在 build.gradle.kts(模块级别)里加上插件:
plugins {
id("jacoco")
}
jacoco {
toolVersion = "0.8.11"
}
tasks.register<JacocoReport>("jacocoTestReport") {
dependsOn("testDebugUnitTest")
reports {
xml.required.set(true)
html.required.set(true)
}
val fileFilter = listOf(
"**/R.class",
"**/R$*.class",
"**/BuildConfig.*",
"**/Manifest*.*",
"**/*Test*.*",
"**/ComposableSingletons*"
)
val debugTree = fileTree("${buildDir}/tmp/kotlin-classes/debug") {
exclude(fileFilter)
}
val mainSrc = "${project.projectDir}/src/main/java"
sourceDirectories.setFrom(files(mainSrc))
classDirectories.setFrom(files(debugTree))
executionData.setFrom(fileTree(buildDir) {
include("jacoco/testDebugUnitTest.exec")
})
}
嗯,这里要注意:Compose 的 Composable 函数会被编译器生成一些辅助类,比如 ComposableSingletons,这些一定要过滤掉,否则覆盖率会被「虚高」。
GitHub Actions 集成:让覆盖率自动跑
配置好本地任务后,下一步就是把它塞进 CI 流水线。我个人推荐在 PR 触发时跑覆盖率,而不是每次 push 都跑——节省时间,也减少噪音。
在项目根目录创建 .github/workflows/test.yml:
name: Run Tests with Coverage
on:
pull_request:
branches: [ main, develop ]
jobs:
test:
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'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run unit tests with coverage
run: ./gradlew jacocoTestReport
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: app/build/reports/jacoco/jacocoTestReport/html/
跑完之后,覆盖率报告会以 Artifact 的形式保存。你可以在 GitHub Actions 的页面里直接下载 HTML 报告,打开就能看到每个类的覆盖情况。
macOS-latest 或 windows-latest,路径分隔符可能不一样。建议统一用 ubuntu-latest 来跑测试,省心。
生成可读的测试报告
JaCoCo 默认生成的 HTML 报告其实已经不错了。它会用颜色标记:
- 绿色:已覆盖
- 红色:未覆盖
- 黄色:部分覆盖(比如 if-else 只走了其中一个分支)
但如果你想让报告更「高级」一点,可以加上 覆盖率徽章。比如用 jacoco-badge-generator 这个 Action:
- name: Generate coverage badge
uses: cicirello/jacoco-badge-generator@v2
with:
badges-directory: badges
generate-branches-badge: true
这样你的 README 里就能放一个实时更新的徽章:

我曾经在一个开源项目里这么干过,效果很好。PR 提交者看到徽章变红了,自己就会去补测试——省了我不少 review 时间。
覆盖率阈值:别让 CI 变成摆设
光生成报告还不够,你得让 CI 在覆盖率不达标时直接失败。否则大家看看报告就过去了,没人会真正去改。
在 build.gradle.kts 里加一个检查任务:
tasks.register<JacocoCoverageVerification>("jacocoCoverageCheck") {
dependsOn("jacocoTestReport")
violationRules {
rule {
limit {
minimum = "0.80".toBigDecimal() // 行覆盖率不低于 80%
}
}
rule {
element = "CLASS"
excludes = listOf(
"*.databinding.*",
"*.di.*",
"*.hilt.*"
)
limit {
counter = "BRANCH"
minimum = "0.70".toBigDecimal() // 分支覆盖率不低于 70%
}
}
}
}
然后在 CI 里加上这个任务:
- name: Verify coverage
run: ./gradlew jacocoCoverageCheck
如果覆盖率低于阈值,CI 就会报红。这时候开发者就必须去补测试或者调整代码。
Compose UI 测试的覆盖率陷阱
这里我要特别提醒一下:Compose UI 测试的覆盖率和单元测试的覆盖率是两回事。
JaCoCo 默认只统计 JVM 单元测试 的覆盖率。UI 测试(比如 createComposeRule 那种)跑在 Android 设备上,用的是不同的执行数据文件。如果你想合并两者,需要额外配置:
// 在 build.gradle.kts 里
tasks.register<JacocoReport>("jacocoFullReport") {
dependsOn("testDebugUnitTest", "connectedDebugAndroidTest")
executionData.setFrom(
fileTree(buildDir) {
include(
"jacoco/testDebugUnitTest.exec",
"outputs/code_coverage/debugAndroidTest/connected/*.ec"
)
}
)
// ... 其余配置同上
}
不过说实话,我一般只关注单元测试的覆盖率。UI 测试更看重「场景覆盖」而不是「代码覆盖」——你想想看,一个 Composable 函数可能只有 10 行代码,但它的交互路径可能有几十条。用行覆盖率去衡量 UI 测试,意义不大。
SVG 流程图:覆盖率与 CI 集成全景
常见问题与避坑
最后分享几个我实际踩过的坑:
- JaCoCo 版本和 AGP 版本不兼容:我曾经因为 JaCoCo 0.8.7 和 AGP 8.0 搭配,导致生成报告时一直报错。后来升到 0.8.11 就好了。建议你直接上最新稳定版。
- Kotlin 内联函数导致覆盖率虚高:内联函数会在调用处展开,JaCoCo 会认为这些代码被「覆盖」了。但实际上你只测了其中一个调用点。这个目前没有完美解法,只能靠人工 review。
- CI 里跑 UI 测试的覆盖率很慢:因为要启动模拟器。我建议把 UI 测试的覆盖率单独做一个 nightly 任务,PR 阶段只跑单元测试的覆盖率。
好了,这一章的内容就到这里。覆盖率不是终点,而是起点。把它和 CI 集成好,你就能在每次代码变更时自动获得一份「质量体检报告」。剩下的,就是持续改进的事了。