android客户端手动测试代码覆盖率统计

本文主要介绍如何通过内置打桩的方式收集app手工测试的代码覆盖率,以便于更直观的发现测试用例的不足

总体介绍

本文主要是用的是jacoco插件,Jacoco是一个开源的覆盖率工具,Jacoco可以嵌入到Ant 、Maven中,并提供了EclEmma Eclipse插件,也可以使用JavaAgent技术监控Java程序。很多第三方的工具提供了对Jacoco的集成,如sonar、Jenkins等。
官网地址:JaCoCo Java Code Coverage Library
在实际是用过程中,与个人环境相关的由于缺少某些jar包的报错,都可以上述网站中下载对应的jar包并导入。
本文主要以博主在实际工作中遇到的某个app为例,总体分为四个步骤:

  • 打开收集覆盖率开关,并在create函数中添加结果收集文件
  • 在跑完测试case,关闭app时将结果写入收集文件
  • 添加生成测试报告的task
  • 将生成的覆盖文件编译生成报告

打开开关,并生成收集文件

1、 添加包
在源码的app_instance中的build.gradle文件中,添加代码覆盖率需要依赖的编译包

1
2
3
4
5
6
7
8
9
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':framework:cupid')
debugCompile 'com.facebook.stetho:stetho:1.0.1'
debugCompile 'com.facebook.stetho:stetho-urlconnection:1.3.1'
debugCompile 'org.jacoco:org.jacoco.agent:0.7.6.201602180812' //add by sxl for coverage
compile 'com.facebook.stetho:stetho:1.3.1'
}

这里需要注意的是,添加完编译时可以会遇到很多关于jar包依赖的报错,如下文提过的,需要手动下载jacoco相关的jar包放在本地lib库中并导入。
2、 打开debug中统计覆盖率开关
在项目正在运行的build.gradle中加入jacoco插件
apply plugin: "jacoco"
并修改buildType中的debug属性:

1
2
3
4
5
6
7
8
9
10
debug {
minifyEnabled false
debuggable true
testCoverageEnabled true //add by sxl for coverage
minifyEnabled false
signingConfig signingConfigs.release
ndk {
abiFilter "armeabi"
}
}

注意,如果debug中有混淆,需要把混淆去掉。
3、 添加收集文件
app启动时创建覆盖率统计coverage.ec文件
需要在app启动的文件(AppInstanceApplicationCallback.java)中的onCreate函数中添加如下代码:

if (BuildConfig.DEBUG){
    String SDCARD_PATH = Environment.getExternalStorageDirectory().getPath();
    String DEFAULT_COVERAGE_FILE_PATH = SDCARD_PATH + "/coverage.ec";
    File file = new File(DEFAULT_COVERAGE_FILE_PATH);
    if (!file.exists()) {
        try {
            file.createNewFile();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

关闭app时,收集统计的数据

1、 收集数据
需要在结束后,关闭app时将统计到的数据写入上一步添加的文件中,因此需要在AppInstanceActivityCallback.java中添加如下方法:

public void generateCoverageReport() {
    String SDCARD_PATH = Environment.getExternalStorageDirectory().getPath();
    String DEFAULT_COVERAGE_FILE_PATH = SDCARD_PATH + "/coverage.ec";
    Log.d("generateCoverageReport", "generateCoverageReport():" + DEFAULT_COVERAGE_FILE_PATH);
    OutputStream out = null;
    try {
        out = new FileOutputStream(DEFAULT_COVERAGE_FILE_PATH, false);
        Object agent = Class.forName("org.jacoco.agent.rt.RT").getMethod("getAgent").invoke(null);
        out.write((byte[]) agent.getClass().getMethod("getExecutionData", boolean.class).invoke(agent, false));
    } catch (Exception e) {
        Log.d("generateCoverageReport", e.toString(), e);
    } finally {
        if (out != null) {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

并在onDestroy中调用该方法:

protected void onDestroy() {
    LocationPerformer.getInstance().stop(this);
    super.onDestroy();
    if (BuildConfig.DEBUG) {
        generateCoverageReport(); //add by sxl for coverage
 }
}

添加报告jacocoTestReport 任务

1、 在源码的app_instance中的build.grade文件中,添加如下代码:

def coverageSourceDirs = ['../test_app/src/main/java']
task jacocoTestReport(type: JacocoReport) {
    group = "Reporting"
 description = "Generate Jacoco coverage reports after running tests."
 reports {
        xml.enabled true
 html.enabled true

 }
    classDirectories = fileTree(
            dir: '../test_app/build/intermediates/classes/debug',
            excludes: ['**/R*.class',

                       '**/*$InjectAdapter.class',
                       '**/*$ModuleAdapter.class',
                       '**/*$ViewInjector*.class'
 ])
    sourceDirectories = files(coverageSourceDirs)
    executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
    doFirst {
        new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
            if (file.name.contains('$$')) {
                file.renameTo(file.path.replace('$$', '$'))
            }
        }
    }
}

完成上述操作后,一般架构简单app的覆盖率就可以收集了.
2、 内部业务aar打包
在收集实际的公司的项目时,发现覆盖率永远只收集了最外层的“架子”的代码,后来才发现,很多公司由于业务较为独立,所以项目中一般会使用app_instance封装最终的业务代码,一般是独立的业务编译成aar后以lib的方式提供给app_instance调用,所以若是想监控具体的业务代码,就需要把桩打到业务内部。
具体方式见上一步中的“打开debug中统计覆盖率开关”小节,需要注意的是,要找到实际代码的build.gradle文件。
3、 添加覆盖率merge任务
在获取到单一的coverage.ec文件后,使用1中所描述的生成报告任务,则可以获取到当前收集到的coverage文件的覆盖率,但是若是多人进行测试,则会生成多个coverage.ec文件,对于总体的覆盖率,需要首先将获取到的覆盖率文件进行合并,合并后计算覆盖率,则需要添加合并等task。

def coverageSourceDirs = [
        '../test_app/src/main/java'
]
task coverageMerge(type: JacocoMerge) {
    description = 'Merge test code coverage results from feign and cucumber'
    executionData fileTree("${buildDir}/outputs/code-coverage/connected")
}
task mergeReport(type: JacocoReport) {
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled true
        html.enabled true
    }
    classDirectories = fileTree(
            dir: '../test_app/build/intermediates/classes/debug',
            excludes: ['**/R*.class',
                       '**/*$InjectAdapter.class',
                       '**/*$ModuleAdapter.class',
                       '**/*$ViewInjector*.class'
            ])
    sourceDirectories = files(coverageSourceDirs)
    executionData = files("$buildDir/jacoco/coverageMerge.exec")
    doFirst {
        new File("$buildDir/intermediates/classes/").eachFileRecurse { file ->
            if (file.name.contains('$$')) {
                file.renameTo(file.path.replace('$$', '$'))
            }
        }
    }
}

生成覆盖性报告文件

1、 打包
通过cradle assembleDebug命令打包
2、生成报告

  • 把coverage.ec 文件拷到报告路径下
    将/sdcard/下的coverage.ec pull到本地,adb pull /sdcard/coverage.ec
    将coverage.ec复制到build/outputs/code-coverage/connected下,文件夹不存在的话,自己创建即可。
  • 生成报告
    在命令行中进入到test_app目录下,通过gradle jacocoTestReport生成报告
  • 生成合并报告
    将收集到的不同的coverage.ec文件均复制到build/outputs/code-coverage/connected,结尾保证为.ec文件不变,在命令行中进入到test-android-passenger目录下,通过./gradlew coverageMerge命令将收集到的ec文件合并成最终的coverageMerge.exec文件,该文件路径为build/jacoco/coverageMerge.exec
    在保证改路径下生成了merge版的覆盖文件后,通过./gradlew mergeReport命令,生成覆盖版的测试报告。

报告说明

单个覆盖率报告所在路径为:app_instance/build/report/jacoco/jacocoTestReport/html/index.html
合并版覆盖率报告路径为:app_instance/build/report/jacoco/jacocoTestReport/html/index.html
Jacoco包含了多种尺度的覆盖率计数器,包含:
指令级(Instructions,C0coverage),分支(Branches,C1coverage)、圈复杂度(CyclomaticComplexity)、行(Lines)、方法(non-abstract methods)、类(classes)。
报告如下图所示,从左向右分别是:
第一列:package
第二列:Instructions
第三列:Branches (if else 之关的分支)
第四列:CyclomaticComplexity
第五列:Lines
第六列:methods
第七列:classes

单击左边的class,然后一层一层进入具体方法:

具体颜色定义:标示绿色的为分支覆盖充分,标黄色的为部分分支覆盖,标红色的为未执行该分支。