速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

增量代码覆盖率工具

  • 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:201612

评论 1 条评论

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

聊聊实时数仓架构设计

水滴

实时数仓 数仓架构 8月日更 数仓建设思路

【前端 · 面试 】HTTP 总结(一)—— HTTP 概述

编程三昧

面试 大前端 HTTP 8月日更

现代分布式架构设计原则-可靠性

余先生

稳定性 可用性 弹性 可靠性

网络攻防学习笔记 Day92

穿过生命散发芬芳

网络攻防 8月日更

服装生产流程管理在明道云的实现

明道云

【LeetCode】矩阵中战斗力最弱的 K 行Java题解

Albert

算法 LeetCode 8月日更

Linux中Shell重定向

入门小站

Linux

【Vue2.x 源码学习】第二十二篇 - dep 和 watcher 关联

Brave

源码 vue2 8月日更

Pandas入门教程-开篇之作

Peter

Python pandas 数据分析师 #python

架构师实战营 模块九总结

代廉洁

架构实战营

做行业的底层架构者 为区块链+提供更多可能

CECBC

02-架构图

Lane

菜鸡学习python

Augus

8月日更

李运华老师(前阿里P9)架构实战营 毕业总结

代廉洁

架构实战营

Convolutional Neural Network (CNN)

毛显新

神经网络 深度学习 tensorflow 图像识别

非典型开发者的形象三变

脑极体

Git的基本操作

卢卡多多

git flow git reset 8月日更

缓存使用的一些问题

旺仔大菜包

redis

毕业设计-秒杀业务

白发青年

架构实战营

架构实战营毕业总结

白发青年

#架构实战营

「SQL数据分析系列」13. 索引和约束

Databri_AI

sql 索引 位图

Pandas入门教程-Series类型数据

Peter

Python 数据分析 数据 pandas

架构实战营-毕业设计

泄矢的呼啦圈

架构实战营

Discourse 图片上传的更新

HoneyMoose

手把手撸二叉树之叶子相似的树

HelloWorld杰少

面试 大前端 二叉树 数据结构与算法 8月日更

pyinstaller 打包

橙橙橙橙汁丶

具备货币属性的比特币,会成为一种货币吗?

CECBC

Python OpenCV 图像处理之傅里叶变换,取经之旅第 52 篇

梦想橡皮擦

8月日更

解密NFT,进军元宇宙,区块链与价值实体将如何链接?

CECBC

在线短视频缩略图剪切工具

入门小站

工具

大数据训练营 -0725 课后作业

cc

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