测试覆盖率 安卓多 module 和多独立 SDK 手工测试覆盖率收集实践

不二家的小球迷 · 2018年12月14日 · 最后由 sanbo 回复于 2018年12月18日 · 608 次阅读

写在前面

覆盖率一直是我的心头痛,去年年初实践了一下iOS方面的手工测试覆盖率收集 iOS 手工测试代码覆盖率获取.

由于团队内推广原因,未真正带来有用的价值,现在重新拾起安卓的覆盖率,是因为独立项目组对SDK的覆盖率收集有需求,而技术负责人也支持推动覆盖率获取收集,有需求就有动力。

基础知识

安卓覆盖率方面的文章和资料很多。主要参考

这些文章里面介绍了主体施行流程,但是我们的项目是多个SDK是以Jar的形式引入到我们的主项目,文章中未解答多模块和多独立SDK覆盖率收集问题。

在自己摸索和热心tester大佬 @zailushang , @simple@VPDong帮助下,解决了一系列问题,最终实践取到多模块和多独立SDK覆盖率报告。

先简单介绍一下,覆盖率工具的选择和对Jacoco的理解。

覆盖率工具选择

由于项目中使用的gradle ,且Java版本也在持续更新中,选择Jacoco作为覆盖率收集工具。

字节码插桩分为两种模式On-The-Fly和Offine。

  • On-The-Fly模式优点在于无需修改源代码,可以在系统不停机的情况下,实时收集代码覆盖率信息。

  • Offine模式优点在于系统启动不需要额外开启代理,但是只能在系统停机的情况下才能获取代码覆盖率。

选择Jacoco Offline的模式实现手工测试覆盖率收集

实现过程【以公司项目为例】

1、app/src/main/java下新建dump测试覆盖率文件的代码,然后在在应用置于后台时候,调用 generateEcFile方法生成覆盖率文件。


package com.aa.bb.cc;

import android.util.Log;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
* Created by sun on 17/7/4.
*/


public class JacocoUtils {
static String TAG = "JacocoUtils";

//ec文件的路径
private static String DEFAULT_COVERAGE_FILE_PATH = "/mnt/sdcard/coverage.ec";

/**
* 生成ec文件
*
* @param isNew 是否重新创建ec文件
*/

public static void generateEcFile(boolean isNew) {
// String DEFAULT_COVERAGE_FILE_PATH = NLog.getContext().getFilesDir().getPath().toString() + "/coverage.ec";
Log.d(TAG, "生成覆盖率文件: " + DEFAULT_COVERAGE_FILE_PATH);
OutputStream out = null;
File mCoverageFilePath = new File(DEFAULT_COVERAGE_FILE_PATH);
try {
if (isNew && mCoverageFilePath.exists()) {
Log.d(TAG, "JacocoUtils_generateEcFile: 清除旧的ec文件");
mCoverageFilePath.delete();
}
if (!mCoverageFilePath.exists()) {
mCoverageFilePath.createNewFile();
}
out = new FileOutputStream(mCoverageFilePath.getPath(), true);

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));

// ec文件自动上报到服务器
//UploadService uploadService = new UploadService(mCoverageFilePath);
//uploadService.start();
} catch (Exception e) {
Log.e(TAG, "generateEcFile: " + e.getMessage());
} finally {
if (out == null)
return;
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

2、在项目根目录的build.gradle追加


apply from: "${rootProject.projectDir}/jacoco.gradle"

jacoco.gradle内容为:

apply plugin: 'jacoco'

jacoco {
toolVersion = '0.8.2'
}


allprojects { p ->
p.afterEvaluate {
p.android {
apply plugin: 'jacoco'
defaultPublishConfig "debug"
buildTypes {
debug {
testCoverageEnabled = true
}
}
}
}
}

task jacocoTestReport(type: JacocoReport) {
reports {
xml.enabled true
html.enabled true
csv.enabled false
}

executionData fileTree("$buildDir/coverage.ec")

def classExcludes = ['**/R*.class',
'**/*Factory*.class',
'**/
*$InjectAdapter*.class',
'**/*$ModuleAdapter*.class',
'**/
*$ViewInjector*.class']

sourceDirectories = files()
classDirectories = files()

project.rootProject.allprojects.each {

sourceDirectories += files(it.projectDir.absolutePath + '/src/main/java')
def path = it.buildDir.absolutePath + '/intermediates/javac/debug/compileDebugJavaWithJavac/classes/'
classDirectories += fileTree(dir: path, excludes: classExcludes, includes: ['**/*.class'])
}

doFirst {
fileTree(dir: project.rootDir.absolutePath, includes: ['**/
classes/**/*.class']).each { File file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}

这里做了两件事,

  • 第一是对所有的主模块,包括项目中的子模块插入收集覆盖率代码的testCoverageEnabled=true

  • 第二是增加了jacocoTestReport的task,收集所有模块下,包括主模块和子模块下的覆盖率报告

3、这个时候我们到SDK模块下,同样修改收集覆盖率的build.gradle,类似主工程中更改的方式。

需要注意的是我们的SDK是以jar的形式引入主项目中。

所以,我们需要同时在SDK中插桩,在主项目工程中指定引入插桩的版本SDK依赖即可。

4、在应用内测试后,然后将应用置于系统后台。

执行adb pull /mnt/sdcard/coverage.ec将覆盖率文件导出到项目主工程下的build/下。

执行./gradlew jacocoTestReport可生成主工程下和各个子模块的覆盖率报告。

5、将覆盖率文件同样复制到SDK项目目录下,这里有刚才编译生成的产出物和源码。同样执行生成覆盖率报告的task。

遇到的问题

1、覆盖率文件的格式不对

java.io.IOException: Incompatible version 1007

查阅资料:

使用命令检查

hexdump -n 5 coverage.ec

发现出现的覆盖率文件一直是06,意味着是低版本的Jacoco版本生成的覆盖率文件,但是我已经在jacoco.gradle指定了高版本。
通过二叉对比法,一直寻找到我们项目历史提交中是否一直有此问题,直到找到问题提交,是因为gradle plugin升级后,我们使用的官方plugin版本 3.0.1反而内置使用了低版本的jacoco。
故,解决方案是升级gradle plugin。

2、编译后中间产出物路径不对
升级gradle plugin后,生成覆盖率报告所需要的中间产出物class文件夹找不到。
虽然可以通过 ./gradlew createDebugCoverageReport生成临时的中间产出物classes/classes.jar一样可以配合生成覆盖率报告,但是这样总显得不够优雅,因为

任务 作用
connectedAndroidTest 执行android的case
createDebugCoverageReport 产生代码覆盖率的报告
connectedCheck 包含上面2个任务

后来,我在提问

才了解到升级plugin后classes文件所在路径。

3、覆盖率报告aat模块点进去不能link到源码
由于我仔细检查了所有模块,只有这个模块有问题,但是找了好几个开发都不能帮我对比出这个模块和其他模块不一样的地方,从build.gradle到Manifest.xml,怎么都无法解决。
我同时仔细对比了sourceDirectories和classDirectories,生成覆盖率报告后能否点击源码主要依赖这里的sourceDirectories,但是我确认路径是对的。依然无法解决。
后来我在groups.google.com jacoco模块提问

原来 aat的模块com.aa.bb.cc不是一个Package,真正路径不是src/main/java/com/aa/bb/cc/,而是以com.aa.bb.cc以字符串命名的一个Package真实路径是src/main/java/com.aa.bb.cc

思考

把测试覆盖率作为质量目标没有任何意义,而我们应该把它作为一种发现未被测试覆盖的代码的手段。
后续将以持续集成的方式完成当前手动执行的步骤。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 2 条回复 时间 点赞

赞,楼主是个爱专研的同学,向楼主学习

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册