20、测试覆盖率与报告:JaCoCo配置、Kover插件使用、行覆盖率与分支覆盖率、生成HTML报告、CI中集成覆盖率检查
聊到测试,很多人会问:「你的测试够不够?」。这个问题其实挺难回答的。我个人的习惯是——不看感觉,看数据。而测试覆盖率,就是那个最直观的数据指标。
说白了,覆盖率就是告诉你:你的代码有多少被测试执行过。但这里有个坑——覆盖率100%不代表没bug。我在项目中遇到过好几次,覆盖率报表漂漂亮亮的,线上还是出了事故。为什么?因为覆盖率和测试质量是两码事。不过话说回来,没有覆盖率数据,你连「测了多少」都不知道,那就更危险了。
这一章,我们就来聊聊Android项目里怎么搞覆盖率。主要讲两个工具:JaCoCo(Java Code Coverage)和Kover(Kotlin专属的覆盖率插件)。
20.1 覆盖率的核心概念
先搞清楚几个术语,不然后面看报表会懵。
| 指标 | 含义 | 通俗理解 |
|---|---|---|
| 行覆盖率 | 被测试执行到的代码行数 / 总行数 | 你跑了多少行代码 |
| 分支覆盖率 | if/else、when等分支中被覆盖的比例 | 你走了几条路 |
| 方法覆盖率 | 被调用的方法 / 总方法数 | 你测了几个函数 |
| 类覆盖率 | 被加载的类 / 总类数 | 你碰了几个文件 |
我个人最关注的是分支覆盖率。行覆盖率再高,如果分支没走全,那隐藏的逻辑漏洞还是测不到。举个例子:
fun getDiscount(level: Int): Double {
return if (level >= 3) {
0.8 // 分支1
} else {
1.0 // 分支2
}
}
如果你只测了level=5的情况,行覆盖率可能是100%(两行都执行了),但分支覆盖率只有50%(else分支没走到)。嗯,这里要注意——分支覆盖率比行覆盖率更能反映测试的充分性。
20.2 JaCoCo 配置实战
JaCoCo是Java生态的老牌覆盖率工具。Android项目里用起来也不复杂。我一般在app模块的build.gradle里这样配:
plugins {
id 'jacoco'
}
jacoco {
toolVersion = "0.8.11"
}
tasks.register("jacocoTestReport", JacocoReport) {
dependsOn "testDebugUnitTest"
reports {
xml.required = true
html.required = true
csv.required = false
}
def fileFilter = [
'**/R.class',
'**/R$*.class',
'**/BuildConfig.*',
'**/Manifest*.*',
'**/*Test*.*',
'android/**/*.*'
]
def debugTree = fileTree(
dir: "${buildDir}/intermediates/javac/debug/classes",
excludes: fileFilter
)
def mainSrc = "${project.projectDir}/src/main/java"
sourceDirectories.setFrom(files([mainSrc]))
classDirectories.setFrom(files([debugTree]))
executionData.setFrom(
fileTree(dir: buildDir, includes: [
"jacoco/testDebugUnitTest.exec",
"outputs/code-coverage/connected/*.ec"
])
)
}
配置完以后,跑一下:
./gradlew jacocoTestReport
然后去 app/build/reports/jacoco/jacocoTestReport/html/ 打开index.html,就能看到漂亮的覆盖率报表了。
重要提醒:JaCoCo默认会过滤掉很多自动生成的代码。上面我写的fileFilter里,R.class、BuildConfig这些一定要排除,不然覆盖率数据会被「注水」。
20.3 Kover 插件——Kotlin 的专属选择
如果你项目里Kotlin代码占大头,我建议试试Kover。这是JetBrains官方出的,对Kotlin的支持比JaCoCo好很多。我在一个纯Kotlin项目里用过,体验确实丝滑。
配置方式更简洁:
// 根目录 build.gradle.kts
plugins {
id("org.jetbrains.kotlinx.kover") version "0.7.5" apply false
}
// app模块 build.gradle.kts
plugins {
id("org.jetbrains.kotlinx.kover")
}
kover {
reports {
filters {
excludes {
classes(
"*.R",
"*.R$*",
"*.BuildConfig",
"*\$Creator"
)
}
}
verify {
rule {
minBound(80) // 覆盖率不低于80%
}
}
}
}
跑命令:
./gradlew koverHtmlReport
报表生成在 app/build/reports/kover/html/。Kover的报表界面比JaCoCo清爽不少,而且对Kotlin的inline函数、协程等特性支持更好。
我的建议:如果项目是Java+Kotlin混编,用JaCoCo就够了。如果纯Kotlin或者Kotlin为主,果断上Kover。两个工具生成的报表格式差不多,但Kover的准确度更高。
20.4 行覆盖率 vs 分支覆盖率——到底看哪个?
这个问题我经常被问到。我的回答是:两个都要看,但优先级不同。
行覆盖率告诉你「测了多少行」,分支覆盖率告诉你「测了多少逻辑」。举个例子:
fun process(input: String?): String {
if (input != null && input.length > 5) {
return input.substring(0, 5)
}
return "default"
}
这段代码有3行可执行代码。如果你只传了"hello"(长度5),行覆盖率是100%(3行都执行了),但分支覆盖率呢?
- 分支1:input != null → true
- 分支2:input.length > 5 → false
实际上有2个分支点,你只覆盖了1个组合(true + false),分支覆盖率只有50%。
我曾经在一个支付模块里吃过这个亏。行覆盖率95%,但分支覆盖率只有60%。结果线上出了个边界条件,刚好是没覆盖到的那个分支。从那以后,我给自己定了个规矩:行覆盖率可以放宽到70%,但分支覆盖率不能低于80%。
20.5 生成HTML报告
不管是JaCoCo还是Kover,生成HTML报告都是标配。HTML报告的好处是——可视化。你可以直接看到哪些行是绿色的(已覆盖),哪些是红色的(未覆盖),哪些是黄色的(部分覆盖)。
我个人习惯在本地开发时,每次跑完测试都顺手生成一下HTML报告。打开浏览器看一眼,红色区域多不多,心里就有数了。
Kover的HTML报告里还有一个很贴心的功能——按包、按类、按方法逐级下钻。你可以从最顶层的模块覆盖率,一路点进去,看到具体哪个if条件没走到。这个对排查测试盲区特别有用。
20.6 CI中集成覆盖率检查
本地看报告是一回事,CI里自动检查是另一回事。我建议在CI流水线里加一个步骤:跑测试 → 生成覆盖率 → 检查是否达标 → 不达标就失败。
用Kover的话,配置很简单:
kover {
verify {
rule {
minBound(80) // 整体覆盖率不低于80%
}
rule {
entity = KotlinClass()
minBound(100) // 每个类不低于100%?这个太严了,慎用
}
}
}
然后在CI脚本里:
./gradlew koverVerify
如果覆盖率没达标,构建就会失败。这样就能在合并代码之前,把测试不足的代码挡在门外。
避坑指南:我曾经在CI里设了「每个类覆盖率不低于90%」的规则,结果第二天所有开发都在骂我。因为有些工具类、配置类根本不需要测那么高。建议只设整体阈值,或者按模块分设阈值,别一刀切。
用JaCoCo的话,可以配合SonarQube来做覆盖率检查。SonarQube里可以配置质量门禁(Quality Gate),覆盖率低于阈值就标记为失败。不过这个配置稍微复杂一些,需要搭SonarQube服务端。
20.7 本章知识体系
下面这张图帮你理清覆盖率相关的核心脉络:
这张图把覆盖率的核心脉络串起来了。从概念到工具,再到实践,每一步都有对应的落地方式。你不需要一次性搞懂所有细节,但至少要知道:覆盖率不是目的,而是手段。它的真正价值,是帮你发现那些「你以为测了、其实没测到」的角落。
好了,这一章就到这里。覆盖率这块,先动手配起来,跑一次报告看看,比看十遍文章都管用。