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:202022

评论 1 条评论

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

GOPS现场 | 大规模团队如何实现Jenkins的集中管理——对话龙智技术顾问

龙智—DevSecOps解决方案

jenkins 管理Jenkins

JDK RMI探索与使用--序列化

霍格沃兹测试开发学社

图像匹配几种常见算法与实践

霍格沃兹测试开发学社

APISIX的安装和简单使用

飞翔

Java程序员不得不会的124道面试题(含答案)

钟奕礼

Java 面试 java;

栓Q了,大厂被强制毕业,空窗一个月死背八股文,还好拿到了Offer

Geek_0c76c3

Java 数据库 开源 架构 开发

ITSM | Atlassian ITSM终极指南,重构IT、运营和支持的工作方式

龙智—DevSecOps解决方案

ITSM ITSM解决方案

通过Inotify-tools 监听文件夹并同步文件至FTP服务器

皮特王

行业方案|“医疗”行业智能运维解决方案介绍

云智慧AIOps社区

监控 智能运维 智能优化算法 自动化运维 IT解决方案

Android R给自家UA工具挖坑

霍格沃兹测试开发学社

天了噜,原来有效的复盘要这样做,微妙!

博文视点Broadview

leetcode 208. Implement Trie (Prefix Tree) 实现 Trie (前缀树) (中等)

okokabcd

LeetCode 数据结构与算法

【Java面试宝典】带你拿offer

钟奕礼

Java 面试 java;

GOPS现场 | 芯片行业需要怎样的版本管理工具——对话龙智大规模安全研发技术专家

龙智—DevSecOps解决方案

版本控制 版本管理工具 版本管理 版本控制工具

Baklib每日分享|在线产品手册的制作技巧

Baklib

技术分享 | web自动化测试-文件上传与弹框处理

霍格沃兹测试开发学社

​小长假要到了,来偶遇吗?

腾讯云数据库

数据库 腾讯云 tdsql 腾讯云数据库

Mongodb的分页优化及索引使用

霍格沃兹测试开发学社

BUG 修复预估模型

霍格沃兹测试开发学社

图像匹配几种常见算法与实践

霍格沃兹测试开发学社

知识管理在企业业务中如何体现其价值

Baklib

重铸资源合集之荣光,吾辈义不容辞!!

掘金安东尼

前端 9月月更

Jenkins实践——创建Pipeline的两种方式

霍格沃兹测试开发学社

狂刷《Java权威面试指南(阿里版)》,冲击“金九银十”有望了

程序知音

Java 阿里 后端技术 Java面试题 Java面试八股文

600+ 道 Java面试题及答案整理(建议收藏)

钟奕礼

Java 面试 java; Java 面试题

Baklib知识分享|知识库对企业来说有哪些意义?

Baklib

前端代码优化小技巧

霍格沃兹测试开发学社

利用 zabbix 监控服务端口

霍格沃兹测试开发学社

8年经验面试官详解 Java 面试秘诀

钟奕礼

Java 面试 java;

GitHub无抗手!MySQL DBA攻坚指南一出,阿里数据库专家都解脱了

Geek_0c76c3

Java 数据库 开源 程序员 架构

彻底搞懂nodejs事件循环

coder2028

node.js

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