最近在写Android端单元测试,自然离不了代码覆盖率,代码覆盖率是衡量测试用例的的重要指标。文章介绍覆盖率工具选型,相关概念介绍,以及在实际项目中如何生成覆盖率数据。
工具 | Jacoco | emma |
---|---|---|
原理 | 使用 ASM 修改字节码 | 修改 jar 文件,class 文件字节码文件 |
覆盖粒度 | 行,类,方法,指令,分支 | 行,类,方法,基本块,指令,无分支覆盖 |
插桩 | on the fly、offline | on the fly、offline |
生成结果 | xml,html,二进制格式 | html、xml、txt,二进制格式 |
缺点 | 需要源代码 | 需要源代码 |
性能 | 快 | 小巧 |
维护状态 | 持续更新中 | 停止维护 |
对比了emma和jacoco,jacoco覆盖粒度相对全面,且有持续更新,gradle配置方便,最终选择了jacoco。
Jacoco 包含了多种粒度的覆盖率计数器,包含指令级(Instructions),分支(Branches)、圈复杂度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、类(Classes)
指令覆盖率,Jacoco计算的最小单位就是字节码指令。
分支覆盖率,异常处理不考虑在分支范围内。
分支点可以被映射到源码中的每一行,并且被高亮表示。
红色钻石:无覆盖,没有分支被执行。
黄色钻石:部分覆盖,部分分支被执行。
绿色钻石:全覆盖,所有分支被执行。
Jacoco为每个非抽象方法计算圈复杂度,并也会计算每个类、包、组的复杂度。根据 McCabe 1996 的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。
行覆盖率,一行源代码是否被执行,要看这一行中是否至少有一个指令被执行。实际上一行代码一般被编译成多个二进制代码指令,源码在高亮显示时,会显示成3种不同的状态:
红色背景:无覆盖,该行的所有指令均无执行。
黄色背景:部分覆盖,该行部分指令被执行。
绿色背景:全覆盖,该行所有指令被执行。
每一个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过。因为Jacoco直接对字节码进行操作,所以有些方法没有在源码显示(比如某些构造方法和由编译器自动生成的方法)也会被计入在内。
每个类中只要有一个方法被执行,这个类就被认定为被执行。注意Jacoco认为构造器和静态初始化都是方法。
配置很简单,打开工程项目的build.gradle文件,添加覆盖率开关
buildTypes {
debug {
...
testCoverageEnabled true
}
添加后,android studio就可以看下createDebugCoverageReport tast,点击运行就可以生成覆盖率数据。
覆盖率数据目录:xxx\app\build\reports\coverage\debug
but,这里有个坑,如果测试脚本有失败时,生成覆盖率数据的命令不会被执行。使用gradlew createDebugCoverageReport --continue,失败后继续后面的task,但依然无效,只能想其他办法。
1.在app目录下定义jacoco.gradle文件,内容如下:
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.7.4+"
}
//源代码路径
def coverageSourceDirs = [
'../app/src/main/java',
]
//class文件路径
def coverageClassDirs = [
'../app/build/intermediates/classes/debug',
]
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
}
// 生成最终的class文件集合
classDirectories = files(files(coverageClassDirs).files.collect {
fileTree(dir: it,
// 过滤不需要统计的class文件
excludes: ['**/R.class',
'**/R$*.class',
'**/Manifest*.*'])
})
// 源码目录路径
sourceDirectories = files(coverageSourceDirs)
//覆盖率ec文件路径
executionData = fileTree(dir:'../app/build/outputs/code-coverage/connected/')
doFirst {
new File("app/build/intermediates/classes/").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
2.在app目录下的build.gradle文件添加apply from: ‘jacoco.gradle’
3.在命令行运行gradlew jacocoTestReport,就可以生成xml和html格式的覆盖率数据。(先执行gradlew createDebugCoverageReport命令)
覆盖率数据目录:xxx\app\build\reports\jacoco\jacocoTestReport
实际中,我们的项目都是多module的,一些公共模块会单独放到一个module下。
如果按照前面的方法,或者运行gradlew createDebugCoverageReport命令后,非app目录下的源码覆盖率是无法生成的。
如何统计多module项目的覆盖率呢?
1.修改需要统计覆盖率的module的build.gradle文件,打开覆盖率开关
buildTypes {
release {
}
debug{
testCoverageEnabled true
}
}
2.创建jacoco.gradle,参照上面自定义task生成覆盖率结果,并添加源代码和class路径
def coverageSourceDirs = [
'../app/src/main/java',
'../base/src/main/java'
]
def coverageClassDirs = [
'../app/build/intermediates/classes/debug',
'../base/build/intermediates/classes/debug'
]
doFirst 也做相应的补充。
3.在app目录下的build.gradle文件添加apply from: ‘jacoco.gradle’
4.在命令行运行gradlew jacocoTestReport,就可以生成xml和html格式的覆盖率数据。(先执行gradlew createDebugCoverageReport命令)
覆盖率数据目录:xxx\app\build\reports\jacoco\jacocoTestReport