Android代码覆盖工具,又是一波骚操作
/ 今日科技快讯 /
近日,百度发布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代码覆盖工具,又是一波骚操作相关推荐
- 选择代码覆盖工具的 10 个标准
为了开发安全可靠的软件,测试是质量保证中不可或缺的一部分.如果没有充分和记录在案的测试,就不可能确定软件是否安全且功能正确.在这种情况下,代码覆盖率(测试覆盖率)的测量尤为重要.这是因为它可以用来确定 ...
- Android 代码检查工具SonarQube
代码检查工具能帮我们检查一些隐藏的bug,代码检查工具中sonar是比较好的一个.官网 Sonar 概述 Sonar 是一个用于代码质量管理的开放平台.通过插件机制,Sonar 可以集成不同的测试工具 ...
- Android代码混淆工具Proguard学习
概述 Proguard代码混淆工具:可以对代码进行去冗余压缩,代码优化,代码混淆等.在Android中的主要应用就是对代码混淆:就是将类名,方法名,Field名变成如a,b,c或者1,2,3等难以阅读 ...
- android混淆语法(android代码混淆工具)
android 代码混淆算法有哪些 根据SDK的版本不同有2中不同的代码混淆方式,以上的proguard.cfg参数详解中所涉及到的信息是在较低版本SDK下的混淆脚本,事实上在高版本的SDK下混淆的原 ...
- android代码检测工具,大家好 给大家介绍一下 Android静态代码检测工具FireLine
FireLine介绍 随着时间的推移,项目的代码量越来越大,而紧张的项目开发周期使得开发人员进行单元测试的时间少之又少.我仔细看了下最近几轮测试中测试人员提的缺陷单,大部分的bug其实归根到底都是由空 ...
- php代码覆盖工具 -- PHPCodeCoverage
1 功能介绍 PHPCodeCoverage(PCC)是一个基于xdebug检测php代码覆盖的工具,它能够应用于功能测试,接口测试,单元测试等任何php代码环境,同时当程序出现异常时,它能够快速的追 ...
- 这帮吃货程序猿,给阿里食堂来了一波骚操作
我叫宋爽,在别人眼里,我是一个程序猿. 别的程序猿,喜欢摁键盘,我嘛,就喜欢吃. 有一次,去医院体检,拿到CT片的我,看着自己的脊椎骨,脑子中一直在想:啊!什么时候去吃顿羊蝎子! 身为吃货的我,最近和 ...
- php代码覆盖工具(2)-phpunit-支持生成覆盖率报告
版本信息: php 7.3.12 xdebug 2.8.0 phpunit 7.1.0 composer 2.0.8 1.安装php环境:教你使用Wampserver(超级详细) - 简书 2.安装 ...
- Android代码混淆工具汇总
DexLabs ProGuard DexGuard Zelix KlassMaster SandMarks 下面附带Android常见的广告库列表 https://github.com/serval- ...
最新文章
- Android踩坑日记:点击变暗效果的ImageView实现原理
- java 监听写文件的进度_java读取文件显示进度条的实现方法
- easy html5 - Jquery mobile
- 安装vs2008出现的问题
- mfc-PlaySound
- M8TSC预览版0.5.1发布
- 【ArcGIS|空间分析|网络分析】3 使用网络数据集查找最佳路径
- 信息技术在园林绿化技师试题测试中的应用
- python画概率密度图_绘制概率密度
- python程序收发文件_使用python脚本发送eml文件
- MySQL锁知识点复习,面试问到的概率超90%
- 小米 11 Ultra/Pro稳定性下降,小米发声明回应
- [安卓相机1]简单小Demo
- 家庭网络搭建_家庭网络
- 大数据告诉你哪部电影最有影响力
- 【转】 SCM工具对比分析
- 鼠标乱跳【坑人必备】
- 马耳他通过了三项法案作为“区块链岛”计划的一部分
- idea 重新下载jar
- 进化:勒索软件的前世今生