/   今日科技快讯   /

近日,百度发布2020年第四季度财务业绩,管理层举行电话会议。百度CEO李彦宏表示,已确定与吉利的电动汽车合资企业的首席执行官和品牌。据悉,2021年1月百度宣布组建一家智能汽车公司,以整车制造商的身份进军汽车行业。

/   作者简介   /

本篇文章来自co_Re同学投稿,分享了他对覆盖率测试工具的相关理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

co_Re的博客地址:

https://blog.csdn.net/u010521645

/   背景   /

当业务快速发展,新业务不断出现,开发同学粗心的情况下,难免会出现少测漏测的情况,如何保证新增代码有足够的测试覆盖率?当一段正常的代码,开发却修改了,测试人员没有测试其功能,如果保证能够发现?

所以代码覆盖测试是有必要的,代码覆盖只能保证这行代码执行了,不能保证其是否正确。寻找相关工具,发现最接近的是jacoco。jacoco 接入也比较简单,在安卓上用的offline 模式,不过jacoco 默认是全部插入探针代码,所以需要对其改造,只对增量代码插入探针。

/   大致流程   /

需求开发流程:项目管理是git,master 分支是线上分支,开发人员在开发某个需求时,会从master 拉取新分支开发,测试完成,封板上线后,会把分支合到master 上。确保master 永远是线上代码。

测试流程:开发人员在开发时,是开发包,开发完成会打测试包给测试人员,测试人员反馈问题,开发修改代码,再次打包,这一过程可能会重复多次。测试通过,告知开发打正式包。最终上线正式包。我们对 开发包称为debug 包,测试包称为beta 包,正式包称为release 包。buildType中对应三种打包方式。

jacocoCoverageConfig {jacocoEnable isBeta()....
}
def isBeta() {def taskNames = gradle.startParameter.taskNamesfor (tn in taskNames) {if (tn == "assembleBeta" || tn == "ttpPackageBeta") {return true}}return false
}

其中debug 包是开发人员直接安装运行,beta与release是通过 jenkins 连打包机打出来供测试人员下载。所以我们在插入探针代码时,只需要对beta 包插入,然后测试人员下载,手动测试,本地生成数据,上传数据给服务器。供后续生成报告时使用,使用开关如下:

jacocoCoverageConfig {jacocoEnable isBeta()....
}
def isBeta() {def taskNames = gradle.startParameter.taskNamesfor (tn in taskNames) {if (tn == "assembleBeta" || tn == "ttpPackageBeta") {return true}}return false
}

在打release 包时,调用生成报告的任务。查看本次增量代码的覆盖率报告,输出报告到apk目录,供开发人员查看,由开发人员判断这个覆盖率是否合理。当然你也可以在低于 百分之多少 时抛出异常,中断打包。

框架的整体流程如下:

首先分为三块:

1、编译时:这里说的是开关为打开的情况,编译时主要是获取两个分支的差异方法集合,然后调用jacoco提供的方法,对差异方法代码插入探针。

2、App 运行时:测试人员在运行带有探针的包,会把探针运行数据.ec 保存在本地,下次再打开app时上传上次数据。

3、生成报告:打正式包时,下载此项目版本所有的覆盖数据,和编译时一样,获取两分支差异方法集合。调用jacoco方法,生成最终的差异方法报告。

下面分别对各个流程中一些技术难点说明。

编译时

编译时是通过gradle TransForm实现的,TransForm可以对字节码进行修改。主要过程分为三大步,class git 管理、获取差异方法、对diff方法插入探针。大致代码如下:

JacocoTransform.groovy
@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {……if (!dirInputs.isEmpty() || !jarInputs.isEmpty()) {if (jacocoExtension.jacocoEnable) {//copy class到 app/classescopy(transformInvocation, dirInputs, jarInputs, jacocoExtension.includes)//提交classes 到gitgitPush(jacocoExtension.gitPushShell, "jacoco auto commit")//获取差异方法集BranchDiffTask branchDiffTask = project.tasks.findByName('generateReport')branchDiffTask.pullDiffClasses()}//对diff方法插入探针inject(transformInvocation, dirInputs, jarInputs, jacocoExtension.includes)}}

class git 管理

首先项目中是有java 和kotlin 源码。如果解析源码文件,需要对两种语言适配。而无论是java 还是kotlin ,编译完都是 .class,解析class 可以通过 ASM 。jacoco 也需要ASM,所以我们需要保存源码对应的 class 文件,当然也不能全部保存,只保存自己的包名的,例如 前辍 com.ttpc 。一些第三方的源码,我们认为它是稳定的,没问题的,也就没必要对其进行覆盖测试,把编译后的class copy到项目的app 目录下,与src 同级,例:

这些 class 也是需要通过git 管理的。然后自动执行git add、commit、push 命令,提交到git 服务器。因为通过 git 可以获得两个服务器分支差异的文件名。

获取两个分支差异方法集

其中编译时与生成报告时都需要获取 “两个分支差异方法集”。其中一个分支就是当前开发分支,一个是master 分支(可配置)。差异方法定义无论是新增方法,还是修改了方法,那怕修改一行代码,都算是差异方法,那个整个方法都要覆盖到。以dev_3 为开发当前分支,master 为稳定分支举例。当前分支通过 git name-rev --name-only HEAD 获取 。

获取差异文件名集

通过 git 可以获得两个分支差异的文件名。

git diff origin/dev_3 origin/master --name-only

输出如下:

通过 \n 分隔,得到差异文件名集合。通过后辍过滤非 .class 与非 包名 文件。

ok,现在得到两分支差异class文件名,但是我们需要精确到差异方法。

copy 两分支差异文件

接下来,切换到master 分支,把所有class copy 到一个临时目录。再切回 当前dev_3分支,把所有class copy 到临时目录。(临时目录和项目同级,为了不影响项目)。删除那些不在 差异文件名集合 的文件,得到差异文件集。切换分支+copy 如下:注意是强制切换,会导致工作区丢失。

#!/bin/shgitBran=$1 # 要切换的分支
workDir=$2 #当前目录
outDir=$3 # copy 输出目录git checkout -b $gitBran origin/$gitBran
git checkout -f $gitBrangit pullcp -r "${workDir}/app/classes" $outDir

目录如下:

生成差异方法集

对两个分支目录的所有class,使用ASM读取class,访问方法,收集方法信息,关键代码如下:

public class DiffClassVisitor extends ClassVisitor {……
@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);final MethodInfo methodInfo = new MethodInfo();methodInfo.className = className;methodInfo.methodName = name;methodInfo.desc = desc;methodInfo.signature = signature;methodInfo.exceptions = exceptions;mv = new MethodVisitor(Opcodes.ASM5, mv) {StringBuilder builder = new StringBuilder();//访问方法一个参数@Overridepublic void visitParameter(String name, int access) {builder.append(name);builder.append(access);super.visitParameter(name, access);}//访问方法一个注解@Overridepublic AnnotationVisitor visitAnnotation(String desc, boolean visible) {builder.append(desc);builder.append(visible);return super.visitAnnotation(desc, visible);}//访问ldc指令,也就是访问常量池索引//与方法体有关,需要参与md5@Overridepublic void visitLdcInsn(Object cst) {//资源id 每次编译都会变,所以不参与 0x7f010008if (!(cst instanceof Integer) || !isResourceId((Integer)cst)) {builder.append(cst.toString());}super.visitLdcInsn(cst);}……//方法访问结束@Overridepublic void visitEnd() {String md5 = Util.MD5(builder.toString());methodInfo.md5 = md5;DiffAnalyzer.getInstance().addMethodInfo(methodInfo, type);super.visitEnd();}}

其中MethodVisitor 有很多 visitXxx方法,都是方法的基本信息与一些指令。然后对其md5 ,得到方法的md5签名。通过classsName,methodName,desc来定位同一方法,然后比较其md5是否一致。一致则代表未修改过代码。这里要注意的是visitLdcInsn 访问常量池指令,因为每次编译时,资源id都会不一致,所以要过滤掉资源id。

当所有的class 访问结束,通过两个分支方法集,得到差异方法集。

public void diff() {if (!currentList.isEmpty() && !branchList.isEmpty()) {for (MethodInfo cMethodInfo : currentList) {boolean findInBranch = false;for (MethodInfo bMethodInfo : branchList) {if (cMethodInfo.className.equals(bMethodInfo.className)&& cMethodInfo.methodName.equals(bMethodInfo.methodName)&& cMethodInfo.desc.equals(bMethodInfo.desc)) {if (!cMethodInfo.md5.equals(bMethodInfo.md5)) {diffList.add(cMethodInfo);}findInBranch = true;break;}}if (!findInBranch) {diffList.add(cMethodInfo);}diffClass.add(cMethodInfo.className);}}} 

插入探针代码

调用jacoco 的instrument ,把插入探针后的字节码写入文件。

 ClassInjector.class@Overridevoid processClass(File fileIn, File fileOut) throws IOException {if (shouldIncludeClass(fileIn)) {InputStream is = null;OutputStream os = null;try {is = new BufferedInputStream(new FileInputStream(fileIn));os = new BufferedOutputStream(new FileOutputStream(fileOut));// For instrumentation and runtime we need a IRuntime instance// to collect execution data:// The Instrumenter creates a modified version of our test target class// that contains additional probes for execution data recording:final Instrumenter instr = new Instrumenter(new OfflineInstrumentationAccessGenerator());final byte[] instrumented = instr.instrument(is, fileIn.getName());os.write(instrumented);} finally {closeQuietly(os);closeQuietly(is);}} else {FileUtils.copyFile(fileIn, fileOut);}}

插入代码关键在ClassInstrumenter.visitMethod,通过判断是否是差异方法来选择是否插入,达到只对差异方法插入代码的目的。

ClassInstrumenter.class
@Overridepublic MethodVisitor visitMethod(final int access, final String name,final String desc, final String signature,final String[] exceptions) {if (DiffAnalyzer.getInstance().containsMethod(className, name, desc)) {InstrSupport.assertNotInstrumented(name, className);final MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);if (mv == null) {return null;}final MethodVisitor frameEliminator = new DuplicateFrameEliminator(mv);final ProbeInserter probeVariableInserter = new ProbeInserter(access,name, desc, frameEliminator, probeArrayStrategy);return new MethodInstrumenter(probeVariableInserter,probeVariableInserter);} else {return super.visitMethod(access, name, desc, signature, exceptions);}}

源码与插入探针后源码对比如下:

至此,运行时工作transForm完成。

运行时

其实jacoco是否覆盖原理,就是每个类都申请了一个boolean数组,然后每一行代码前都插入 array[x]=true,当代码执行,array[x] 也就为true,也就表明代码执行过。然后在页面关闭时,保存所有boolean 数组到本地成ec文件。下次打开app,把上一次的数据上传到服务器。因为这个工具只是用在开发测试阶段,所以服务器直接布在局域网即可。

CodeCoverageManager.class
写入数据到文件
private void writeToFile() {if(filePath==null) return;OutputStream out = null;try {out = new FileOutputStream(filePath, false);IAgent agent = RT.getAgent();out.write(agent.getExecutionData(false));Log.i(TAG, " generateCoverageFile write success");} catch (Exception e) {e.printStackTrace();Log.e(TAG, " generateCoverageFile Exception:" + e.toString());} finally {close(out);}}

生成报告

生成报告是主动触发的,在任何时候都可以生成报告,也可以在打正式包时自动调用生成报告任务。生成报告大致逻辑如下:

1、从服务器上下载 此项目此版本 所有的ec 文件,也就是在运行时上传的那些数据文件。

2、同编译时逻辑一样,获取差异方法集

3、最后调用jacoco 的生成报告方法,参数: ec文件夹,class 文件夹路径,源码路径,报告输出路径。即可生成一份html报告。

File exec = new File("/Users/wzh/gitlab/Android-Jacoco/app/build/outputs/coverage");List<File> sourceDirs = new ArrayList<>();
sourceDirs.add(new File("/Users/wzh/gitlab/Android-Jacoco/app/src/main/java"));List<File> classDirs = new ArrayList<>();
classDirs.add(new File("/Users/wzh/gitlab/Android-Jacoco/app/classes"));File reportDir = new File("/Users/wzh/gitlab/Android-Jacoco/app/build/report");ReportGenerator generator = new ReportGenerator(exec.getAbsolutePath(), classDirs, sourceDirs, reportDir);
generator.create();

在生成报告内部会对ec文件夹内的ec进行合并:

因为多次运行会有多份boolean 数组,所以需要合并取或。如果某一个类,修改了源码,导致两个包代码不一样,会导致这个类的boolean数组合并失败,这时需要把老的类boolean数组丢弃掉。

ExecutionDataStore.class
//合并数据,异常删除老数据
public void put(final ExecutionData data) throws IllegalStateException {final Long id = Long.valueOf(data.getId());final ExecutionData entry = entries.get(id);if (entry == null) {entries.put(id, data);names.add(data.getName());} else {try{entry.merge(data);}catch (IllegalStateException e){
//                e.printStackTrace();if(entry.getSessionInfo()!=null && data.getSessionInfo()!=null){if(entry.getSessionInfo().getDumpTimeStamp()<data.getSessionInfo().getDumpTimeStamp()){System.out.println("old ec data ,remove "+entry);entries.remove(id);entries.put(id, data);}}}}}

最终报告是个html,打开输出报告如下:

最外面可以看到整体的覆盖率,点进去可以查看某个类的覆盖率。

  • 绿色:表示行覆盖充分。

  • 红色:表示未覆盖的行。

  • 空白色:代表方法未修改,无需覆盖。

  • 黄色棱形:表示分支覆盖不全。

  • 绿色棱形:表示分支覆盖完全。

总结

本工具基于jacoco源码,做到了两个git分支 增量方法级的代码覆盖。当然,一些问题也是有的。既然是方法级,如果只改了方法的一行代码,那么整个方法,所有的分支都需要重新覆盖到。在编译时,会自动执行一些git 命令。例如强制切换分支(为了兼容jenkins 远程打包),这会导致工作区内容的丢失。还有开发流程,可能并不适用于你公司的开发流程。(在我公司项目中,在debug与release都是关闭的,只有beta包打开)当然,大家也是可以修改的,适配出自己公司流程的代码覆盖工具。

代码覆盖只能保证这行代码执行了,不能保证其是否正确。可以用来避免一些开发可能修改了线上某个异常,未通知测试人员,测试人员不会去测相关功能。而测试结束了,覆盖率却很低,说明有部分代码修改过,却没有执行过,这是容易引起问题的。而最终的报告,只是一个告知开发的作用,需要由开发人员来判断这个覆盖率到底行不行,会不会引起问题?

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

一篇小短文,带你了解屏幕刷新背后的故事

这可能是ViewPager2滑动冲突最全的处理方案

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

Android代码覆盖工具,又是一波骚操作相关推荐

  1. 选择代码覆盖工具的 10 个标准

    为了开发安全可靠的软件,测试是质量保证中不可或缺的一部分.如果没有充分和记录在案的测试,就不可能确定软件是否安全且功能正确.在这种情况下,代码覆盖率(测试覆盖率)的测量尤为重要.这是因为它可以用来确定 ...

  2. Android 代码检查工具SonarQube

    代码检查工具能帮我们检查一些隐藏的bug,代码检查工具中sonar是比较好的一个.官网 Sonar 概述 Sonar 是一个用于代码质量管理的开放平台.通过插件机制,Sonar 可以集成不同的测试工具 ...

  3. Android代码混淆工具Proguard学习

    概述 Proguard代码混淆工具:可以对代码进行去冗余压缩,代码优化,代码混淆等.在Android中的主要应用就是对代码混淆:就是将类名,方法名,Field名变成如a,b,c或者1,2,3等难以阅读 ...

  4. android混淆语法(android代码混淆工具)

    android 代码混淆算法有哪些 根据SDK的版本不同有2中不同的代码混淆方式,以上的proguard.cfg参数详解中所涉及到的信息是在较低版本SDK下的混淆脚本,事实上在高版本的SDK下混淆的原 ...

  5. android代码检测工具,大家好 给大家介绍一下 Android静态代码检测工具FireLine

    FireLine介绍 随着时间的推移,项目的代码量越来越大,而紧张的项目开发周期使得开发人员进行单元测试的时间少之又少.我仔细看了下最近几轮测试中测试人员提的缺陷单,大部分的bug其实归根到底都是由空 ...

  6. php代码覆盖工具 -- PHPCodeCoverage

    1 功能介绍 PHPCodeCoverage(PCC)是一个基于xdebug检测php代码覆盖的工具,它能够应用于功能测试,接口测试,单元测试等任何php代码环境,同时当程序出现异常时,它能够快速的追 ...

  7. 这帮吃货程序猿,给阿里食堂来了一波骚操作

    我叫宋爽,在别人眼里,我是一个程序猿. 别的程序猿,喜欢摁键盘,我嘛,就喜欢吃. 有一次,去医院体检,拿到CT片的我,看着自己的脊椎骨,脑子中一直在想:啊!什么时候去吃顿羊蝎子! 身为吃货的我,最近和 ...

  8. php代码覆盖工具(2)-phpunit-支持生成覆盖率报告

    版本信息: php 7.3.12 xdebug 2.8.0 phpunit 7.1.0 composer  2.0.8 1.安装php环境:教你使用Wampserver(超级详细) - 简书 2.安装 ...

  9. Android代码混淆工具汇总

    DexLabs ProGuard DexGuard Zelix KlassMaster SandMarks 下面附带Android常见的广告库列表 https://github.com/serval- ...

最新文章

  1. Android踩坑日记:点击变暗效果的ImageView实现原理
  2. java 监听写文件的进度_java读取文件显示进度条的实现方法
  3. easy html5 - Jquery mobile
  4. 安装vs2008出现的问题
  5. mfc-PlaySound
  6. M8TSC预览版0.5.1发布
  7. 【ArcGIS|空间分析|网络分析】3 使用网络数据集查找最佳路径
  8. 信息技术在园林绿化技师试题测试中的应用
  9. python画概率密度图_绘制概率密度
  10. python程序收发文件_使用python脚本发送eml文件
  11. MySQL锁知识点复习,面试问到的概率超90%
  12. 小米 11 Ultra/Pro稳定性下降,小米发声明回应
  13. [安卓相机1]简单小Demo
  14. 家庭网络搭建_家庭网络
  15. 大数据告诉你哪部电影最有影响力
  16. 【转】 SCM工具对比分析
  17. 鼠标乱跳【坑人必备】
  18. 马耳他通过了三项法案作为“区块链岛”计划的一部分
  19. idea 重新下载jar
  20. 进化:勒索软件的前世今生

热门文章

  1. 类脑智能机器人的未来发展前景
  2. 第155章 SQL函数 UPPER
  3. IDEA申请学生免费账号登陆失败
  4. windows mysql默认密码_windows下mysql初始密码设置
  5. 阿里巴巴开发手册:Mysql索引规范
  6. 鸡兔同笼,四大淡水湖真假,最有解,值班日问题
  7. 什么是MVC设计模式
  8. 小程序开发:概念、特点、原理及技术架构解析
  9. win8计算机分区为什么打不开,Win8磁盘驱动器号丢失在计算机中无法找到并打开分区...
  10. jquery如何设置占位隐藏_jquery控制元素的隐藏和显示的几种方法。