NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

增量代码覆盖率工具

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

评论 1 条评论

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

《零基础学 Java》 FAQ 之 5-如何在IntelliJ IDEA里配置JDK

臧萌

Java jdk intellij

LeetCode 781. Rabbits in Forest

liu_liu

LeetCode

职涯思考

Kevin Z

职业 思考 工程师思维 硬件 工程师

Binlog2sql恢复误删除的数据

一个有志气的DB

MySQL 安全 数据

看完这篇,你也是字符编码大神(ASCII、Unicode、UTF-8、UTF-16、UTF-32)

Meandni

Java 面试 Unicode utf-8 utf-16

原创 | 使用JUnit、AssertJ和Mockito编写单元测试和实践TDD (八)好单元测试的特质

编程道与术

Java 编程 软件测试 TDD 单元测试

我画了35张图就是为了让你深入 AQS

一枝花算不算浪漫

AQS jdk源码

当你不被尊重的时候,才会想起去尊重别人

小天同学

人生 个人成长 感悟

Web3极客日报#129

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

VSCode 集成 Haskell 环境

liu_liu

vscode haskell hie Haskell Language Server

克服恐惧,勇于分享

孙苏勇

程序员 分享 工作

Leetcode 556. Next Greater Element III

隔壁小王

算法

MySQL安全-审计

一个有志气的DB

安全 MySQ

webpack入门系列之二——插件使用及热更新打包

子铭

Web

玄铁重剑,我用过最贵重的键盘

池建强

ipad 苹果 键盘

“我XXXX,还能学编程吗?”

三号无名指

编程 程序员 工具 入门

识别代码中的坏味道(一)

Page

Java 面向对象 重构

《零基础学 Java》 FAQ 之 6-Java里Bean这个名字怎么来的

臧萌

Java spring

不要做软件开发团队中打破窗户的那个人

程序员小岑

程序员 思考 感悟 软件开发 团队

LeetCode 513. Find Bottom Left Tree Value

liu_liu

LeetCode

Mac 使用笔记

FeiLong

香港科技大学与OSL海科签订无毒海洋防污技术独家授权协议

极客编

程序员小白的个人思考

程序员小岑

程序员 职场 思考 感悟

要做医生,不要做卖药的

Janenesome

高效工作 思考 沟通

测试开发专题:spring-boot自定义返回参数校验错误信息

王德发

Spring Boot 测试

工作原则:决策要基于数据的支撑

Janenesome

高效工作 思考 原则

Python3.6.1官方文档练习——初入江湖(二)

小匚

十大经典排序算法总结

淡蓝色

Java 算法 计算机基础 排序

修改网桥默认地址

奔跑的菜鸟

Docker

Web3极客日报#141

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

Redis实现热卖商品排行榜

北漂码农有话说

redis

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