使用Jacoco生成覆盖率数据

导语

最近在写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工具

Jacoco工具介绍

Jacoco 包含了多种粒度的覆盖率计数器,包含指令级(Instructions),分支(Branches)、圈复杂度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、类(Classes)

instructions

指令覆盖率,Jacoco计算的最小单位就是字节码指令。

Branches

分支覆盖率,异常处理不考虑在分支范围内。
分支点可以被映射到源码中的每一行,并且被高亮表示。
  红色钻石:无覆盖,没有分支被执行。
  黄色钻石:部分覆盖,部分分支被执行。
  绿色钻石:全覆盖,所有分支被执行。

Cyclomatic Complexity

Jacoco为每个非抽象方法计算圈复杂度,并也会计算每个类、包、组的复杂度。根据 McCabe 1996 的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。

Lines

行覆盖率,一行源代码是否被执行,要看这一行中是否至少有一个指令被执行。实际上一行代码一般被编译成多个二进制代码指令,源码在高亮显示时,会显示成3种不同的状态:
  红色背景:无覆盖,该行的所有指令均无执行。
  黄色背景:部分覆盖,该行部分指令被执行。
  绿色背景:全覆盖,该行所有指令被执行。

methods

每一个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过。因为Jacoco直接对字节码进行操作,所以有些方法没有在源码显示(比如某些构造方法和由编译器自动生成的方法)也会被计入在内。

Classes

每个类中只要有一个方法被执行,这个类就被认定为被执行。注意Jacoco认为构造器和静态初始化都是方法。

接下来介绍,如何在实际项目中使用Jacoco生成覆盖率数据

配置很简单,打开工程项目的build.gradle文件,添加覆盖率开关

buildTypes {
    debug {
        ...
        testCoverageEnabled true
    }

添加后,android studio就可以看下createDebugCoverageReport tast,点击运行就可以生成覆盖率数据。
使用Jacoco生成覆盖率数据_第1张图片
覆盖率数据目录:xxx\app\build\reports\coverage\debug
but,这里有个坑,如果测试脚本有失败时,生成覆盖率数据的命令不会被执行。使用gradlew createDebugCoverageReport --continue,失败后继续后面的task,但依然无效,只能想其他办法。

自定义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的,一些公共模块会单独放到一个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

你可能感兴趣的:(测试,android开发)