写点什么

增量代码覆盖率工具

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

评论 1 条评论

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

API对接之模板方法

Rubble

4月日更 4月月更

老旧项目二次开发指南

阿毛

重构 项目架构 二次开发

Nocalhost - 让云原生时代的开发更高效

沃趣科技

云原生 Nocalhost 应用开发

Go 入门很简单:Writer和Reader接口

宇宙之一粟

接口 Go 语言 4月月更

读《Software Engineering at Google》(08)

术子米德

架构师成长笔记

另一视角看元宇宙:元宇宙文化正悄然改变世界

CECBC

论利润中心内部核算和集团核算

秋去冬来春未远

阿米巴 利润中心 集团成本

upnp.exe进程

Sher10ck

日积月累

Docker 实战教程之从入门到提高 (七)

汪子熙

Docker 容器 docker image 容器镜像 4月月更

一文读懂在OpenHarmony轻量设备开发应用

OpenHarmony开发者

OpenHarmony OpenHarmony应用开发 轻量设备

linux之rpm命令

入门小站

Linux

利用 Dio 完成数据删除操作

岛上码农

ios 跨平台 移动端开发 flutter开发 安卓开发

spring-cloud-kubernetes的服务发现和轮询实战(含熔断)

程序员欣宸

java 4月月更

安全之花如何盛开在华为云空间的每个角落?

脑极体

[Day19]-[动态规划]分割等和子集

方勇(gopher)

LeetCode 动态规划 数据结构和算法

高效压缩位图在推荐系统中的应用

vivo互联网技术

redis 推荐 存储

在线CSV转Plaintext(txt)工具

入门小站

工具

区块链如何助推著原创保护

CECBC

读《Software Engineering at Google》(10)

术子米德

架构师成长笔记

什么是知识库管理系统?如何搭建企业知识库系统?

小炮

企业知识管理 企业知识管理工具 知识管理系统

关于数字货币的几点问题及回应

CECBC

企业管理理念之人本善还是本恶

秋去冬来春未远

企业管理 人性本善 人性本恶 一念之差

Web3.0 时代,我们的生活将产生什么变化?

CECBC

一文论述元宇宙、NFT及不可回避的Web3 时代

CECBC

读《Software Engineering at Google》(09)

术子米德

架构师成长笔记

Linux驱动开发-外部中断的注册使用(按键为例)

DS小龙哥

4月月更

在线YAML转CSV工具

入门小站

工具

java培训JVM内存模型和GC机制的解析

@零度

Java JVM GC

TASKCTL C/S客户端两种不同的登陆模式

敏捷调度TASKCTL

分布式 ETL 批量操作 自动化运维 调度任务

基于云效Codeup一键恢复删库保护数据资源,程序员删库跑路不复存在

阿里云云效

云计算 阿里云 程序员 代码安全 删库保护

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