50万奖金+官方证书,深圳国际金融科技大赛正式启动,点击报名 了解详情
写点什么

增量代码覆盖率工具

  • 2020-03-11
  • 本文字数:4249 字

    阅读完需:约 14 分钟

增量代码覆盖率工具

背景

目前有赞共享技术团队测试介入的微服务应用有几百个,大部分底层应用的单测覆盖率在 70% 以上,同时测试组提供的多纬度集成测试自动化的覆盖率也在 70% 以上。有赞的业务发展非常快,当存量代码较多时,新项目功能测试的整体覆盖率偏低是正常现象,另外开发提测时,并不能依据已有的全量覆盖率来判断对新增代码的自测完成度,基于这个背景,我们研发了增量代码覆盖率工具,作为项目质量的参考纬度之一,支持统计功能测试、单测和集成测试,并集成到了 DevOps 平台。

方案设计

有赞的 JAVA 代码覆盖率工具用的是 JaCoCo ,它是一个开源的覆盖率工具,支持 JVM ,使用方法非常灵活,很多第三方的工具提供了对 JaCoCo 的集成,如 sonar、Jenkins 等。


关于 JaCoCo 的注入原理以及注入方式,在官方网站上写的非常详细了,网上翻译修改的资料也非常多,不做过多赘述。经过对比,我们在统计功能测试覆盖率以及集成测试覆盖率时,选择的是 On-the-fly 模式。原因是 On-the-fly 方式无须入侵应用启动脚本,只需在 JVM 中通过 -javaagent 参数指定 jar 文件启动 Instrumentation 的代理程序,代理程序在通过 Class Loader 装载一个 class 前判断是否需要注入 class 文件,将统计代码插入 class ,测试覆盖率分析就可以在 JVM 执行测试的过程中完成。



(图片来源 官网 )


我们设计的方案也是基于 JaCoCo 做相应改造,生成我们所需要的覆盖率模型,并通过 JaCoCo 开放的 API 实现相关功能。这里面主要需要解决的点在获取增量代码并解析生成覆盖率上。可以拆分成如下几个步骤:


  1. 获取测试完成后的 exec 文件(二进制文件,里面有探针的覆盖执行信息);

  2. 获取基线提交与被测提交之间的差异代码;

  3. 对差异代码进行解析,切割为更小的颗粒度,我们选择方法作为最小纬度;

  4. 改造 JaCoCo ,使它支持仅对差异代码生成覆盖率报告;



整体的流程如上图,下面针对整个流程,分别说下我们是怎么做的。

对 JaCoCo 的改造

在讲具体实现步骤之前,先谈下我们对 JaCoCo 做的改造思路。 JaCoCo 的注入逻辑用的是 ASM 库,对于没有接触过字节码注入技术的测试同学来说,改造注入逻辑需要花费较多时间,而对该工具从调研到完成的预期时间,只有不到 10 人日,所以我们用了一个比较快速简单的方式:前面生成全量覆盖率数据的流程不变,只对解析 exec 文件生成报告做改造,生成我们所需要的覆盖率模型。


JaCoCo 对 exec 的解析主要是在 Analyzer 类的 analyzeClass(finalbyte[]source) 方法。这里面会调用 createAnalyzingVisitor 方法,生成一个用于解析的 ASM 类访问器,继续跟代码,发现对方法级别的探针计算逻辑是在 ClassProbesAdapter 类的 visitMethod 方法里面。所以我们只需要改造 visitMethod 方法,使它只对提取出的每个类的新增或变更方法做解析,非指定类和方法不做处理。


改造后的核心代码片段如下:


获取 exec

我们在部署 qa 项目 java 应用服务时,指定了 -javaagent 参数的 output 为 tcpserver ,并指定可用端口。官方对 output 的参数说明见下图,默认是 file ,目前有赞的集成测试覆盖率用的是这种方式,所以必须要将 JVM 停掉以后才能将信息 dump 到指定文件。



(图片截自 JaCoCo 官网)


我们获取 exec 文件是通过 tcp 方式获取的,且每一次收集的覆盖率数据是追加的形式,所以 javaagent 参数设定如下:output=tcpserver,address=0.0.0.0,port=XXXX ,然后将 javaagent 参数注入 JVM ,这部分由运维团队配合支持,完成了持续交付项目下的 java 应用自动注入 JVM 。


以上步骤完成以后,在我们工具内就可以通过 JaCoCo 开放出来的 API 进行 exec 文件获取,部分代码片段如下:


public void dumpData(String localRepoDir, List<IcovRequest> icovRequestList) throws IOException {    icovRequestList.forEach(req -> req.validate());    icovRequestList.parallelStream().map(icovRequest -> {      String destFileDir = ...;      String address = icovRequest.getAddress();      try {        final FileOutputStream localFile = new FileOutputStream(destFileDir + "/" + DEST_FILE_NAME);        final ExecutionDataWriter localWriter = new ExecutionDataWriter(localFile);        final Socket socket = new Socket(InetAddress.getByName(address), PORT);        final RemoteControlWriter writer = new RemoteControlWriter(socket.getOutputStream());        final RemoteControlReader reader = new RemoteControlReader(socket.getInputStream());        reader.setSessionInfoVisitor(localWriter);        reader.setExecutionDataVisitor(localWriter);        writer.visitDumpCommand(true, false);        if (!reader.read()) {          throw new IOException("Socket closed unexpectedly.");        }        ...      } ...      return null;    }).count();  }
复制代码


在项目测试过程中,会遇到需要重新发布代码的情况,此时大部分人不希望之前测试覆盖的记录被清空,希望对 dump 出来的覆盖率进行累加。对于虚拟机,只要在 javaagent 参数里面设置 append=true(默认就为 true)即可,但对于用 docker 部署的应用,每次重新发布,原先的 exec 文件会丢失,且 ip 也可能会变,需要找运维团队进行配合支持。

获取差异代码并切割到方法粒度

这部分会涉及到较多的 Git 操作,我们是用 JGit 实现的。JGit 是一个用 Java 写成的功能比较健全的 Git 的实现,它在 Java 社区中被广泛使用。在这一步的主要流程是获取基线提交与被测提交之间的差异代码,然后过滤一些需要排除的文件(比如非 Java 文件、测试文件等等),对剩余文件进行解析,将变更代码解析到方法纬度,部分代码片段如下:


private List<AnalyzeRequest> findDiffClasses(IcovRequest request) throws GitAPIException, IOException {    String gitAppName = DiffService.extractAppNameFrom(request.getRepoURL());    String gitDir = workDirFor(localRepoDir,request) + File.separator + gitAppName;DiffService.cloneBranch(request.getRepoURL(),gitDir,branchName);    String masterCommit = DiffService.getCommitId(gitDir);    List<DiffEntry> diffs = diffService.diffList(request.getRepoURL(),gitDir,request.getNowCommit(),masterCommit);    List<AnalyzeRequest> diffClasses = new ArrayList<>();    String classPath;    for (DiffEntry diff : diffs) {      if(diff.getChangeType() == DiffEntry.ChangeType.DELETE){        continue;      }      AnalyzeRequest analyzeRequest = new AnalyzeRequest();      if(diff.getChangeType() == DiffEntry.ChangeType.ADD){        ...      }else {        HashSet<String> changedMethods = MethodDiff.methodDiffInClass(oldPath, newPath);        analyzeRequest.setMethodnames(changedMethods);      }      classPath = gitDir + File.separator + diff.getNewPath().replace("src/main/java","target/classes").replace(".java",".class");      analyzeRequest.setClassesPath(classPath);      diffClasses.add(analyzeRequest);    }    return diffClasses;  }
复制代码

生成覆盖率报告

这步是用 JaCoCo 开放的 API 和改造后的 JaCoCo 来实现的,根据前两步获取到的 class 和差异方法信息,用改造后的 JaCoCo 去解析 exec 文件,使它按照我们的覆盖率模型,只生成增量代码部分的覆盖率报告。 生成报告的大致流程如图:



生成报告和获取报告的触发时点是不同的,生成报告涉及较多的 Git 和 IO 操作,处理时间会比较长,跟 DevOps 的交互上是通过异步方式进行处理。而获取报告是通过批量查询数据库信息来获取所需的报告信息。所以生成报告接口需要保存覆盖率报告以及行覆盖率信息并入库,将覆盖率报告地址在 tengine 里面配置后,DevOps 平台即可实现访问,部分代码片段如下:


private IBundleCoverage analyzeStructure(List<AnalyzeRequest> analyzeRequests,String sourceDirectory) throws IOException {    final CoverageBuilder coverageBuilder = new CoverageBuilder();    for (AnalyzeRequest analyzeRequest:analyzeRequests) {      final Analyzer analyzer = new Analyzer(          execFileLoader.getExecutionDataStore(), coverageBuilder, analyzeRequest.getMethodnames());      File f = new File(analyzeRequest.getClassesPath());      InputStream in = new FileInputStream(f);      analyzer.analyzeClass(in, sourceDirectory);    }    for (final IClassCoverage cc : coverageBuilder.getClasses()) {      totalCoveredCount = totalCoveredCount + cc.getLineCounter().getCoveredCount();      totalCount = totalCount + cc.getLineCounter().getTotalCount();    }    coveredRatio = totalCoveredCount*100/totalCount;    if(reportResDAO.getInfoByPrjName(prjName).size() >= 1) reportResDAO.updateTotalCov(prjName,appName,coveredRatio,new Date());    else{      ...      reportResDAO.insertReportInfo(reportResDO);    }    return coverageBuilder.getBundle(title);  }
复制代码

效果

最终效果如下图,在图中是某个 service 的实现类,实际上在最新的代码中有 14 个方法,但是只会对变更或新增的 4 个方法进行覆盖率统计与显示:



另外在覆盖率报告中显示的覆盖率数据也只是对变更的方法进行统计,不会按照全量代码进行覆盖率计算。对于没有进行测试覆盖的类,覆盖率显示为 0:


与 DevOps 工具集成

目前我们的增量覆盖率工具已经集成到运维的 DevOps 平台,所有接入持续交付的项目在测试完成后,触发生成提测分支的增量代码覆盖率、展示报告,整个流程全自动化。与 DevOps 平台的整体交互大致如下图:



OPS 即有赞的 DevOps 平台,icov 是我们增量代码覆盖率工具提供的服务。 icov 通过 tcp 方式从服务器端获取 exec 文件, OPS 触发 icov 生成报告,并从 icov 获取报告。


生成报告的触发时点是在 qa 环境功能测试完成以后,由于每个项目下有多个应用,所以开放给 DevOps 平台的接口全部为批量异步接口,另外我们的工具提供了多维度的接口封装,可支持其他平台接入,后续会将工具插件化,测试博客也会持续更新。


增量代码覆盖率只能作为一个参考纬度,反推功能测试、单元测试或者集成测试是否存在遗漏,并进行补充,也可以作为开发自测完成度的一个参考,谨慎作为评估指标。


2020-03-11 22:201991

评论 1 条评论

发布
用户头像
变更代码解析到方法纬度实现思路是什么呢
2022-01-04 00:55
回复
没有更多了
发现更多内容

在线二进制转文本工具

入门小站

工具

可怕!RxHttp2,安卓开发基础

android 程序员 移动开发

史上最全Android性能优化总结,经典收藏,一口气拿了9家公司的offer

android 程序员 移动开发

史上最详Android版kotlin协程入门进阶实战(四),flutter开源项目商业化

android 程序员 移动开发

四张图让你玩转Tomcat系统架构!!!,移动应用开发课程设计报告

android 程序员 移动开发

四面阿里,因为最后一个问题与offer失之交臂,android驱动开发

android 程序员 移动开发

又来新需求了,急,Android怎么实现时间线效果(1),android开发面试自我介绍

android 程序员 移动开发

又来新需求了,急,Android怎么实现时间线效果,成体系化的神级Android进阶笔记

android 程序员 移动开发

linux之我常用的系统重要文件备份命令

入门小站

Linux

只有这些东西?不,学习Android开发只要这些东西,想转行当程序员的必看

android 程序员 移动开发

史上最详Android版kotlin协程入门进阶实战(三),4面字节跳动拿到Offer

android 程序员 移动开发

双非大三,无实习经历,如何以 hard 模式逆袭字节跳动,androidframework开发书籍

android 程序员 移动开发

听我慢慢道来Android-面试中的细节,三年老Android经验面经

android 程序员 移动开发

[ CloudWeGo 微服务实践 - 05 ] 服务注册(2)

baiyutang

golang 微服务 11月日更

另一种绕过-Android-P以上非公开API限制的办法,安卓多线程面试题

android 程序员 移动开发

史上最详Android版kotlin协程入门进阶实战(一),一线互联网公司面经总结

android 程序员 移动开发

四月字节客户端面经,七月内推请找我,一文带你搞懂Android多线程Handler

android 程序员 移动开发

回眸重探锁机制,跨平台移动开发的特点

android 程序员 移动开发

双非本,3年时间从外包到阿里P6(Android岗,移动终端软件开发颜色演示

android 程序员 移动开发

反向面试提问,安卓framework层

android 程序员 移动开发

只需四点,让你的面试百试百过!,android移动应用基础教程

android 程序员 移动开发

因为不了解Android事件分发机制,居然被实习生嘲笑了,天呐

android 程序员 移动开发

参考微信模块化通信具体实现,android开发从入门到精通pdf下载

android 程序员 移动开发

反思一次羞愧的阿里面试经历,致Android开发者

android 程序员 移动开发

反思|Android 事件拦截机制的设计与实现,android串口工具apk

android 程序员 移动开发

可能一眼看不懂的几行Kotlin代码,程序员面试防坑宝典

android 程序员 移动开发

启动速度与执行效率优化项目实战(四),android开发入门与实战网盘

android 程序员 移动开发

史上最详Android版kotlin协程入门进阶实战(三)(1),面试Android岗

android 程序员 移动开发

哭了!失业半年-2020没有风口我也要跳槽,凭本事吃饭有技术到哪都是人才

android 程序员 移动开发

四张图让你玩转Tomcat系统架构!!!(1),android音频面试题

android 程序员 移动开发

国内大厂在移动端跨平台的框架接入分析,2021年一起努力应对互联网寒冬吧

android 程序员 移动开发

增量代码覆盖率工具_文化 & 方法_有赞技术_InfoQ精选文章