本书作者:测试开发刚哥,文章版权归个人所有,未经授权请勿转载。
什么是增量覆盖率

如图所示,在 master 分支提交了 HelloController,然后从 master 拉了个新分支 test;提交了第 1 次代码,增加了 WorldController;提交了第 2 次代码,增加了 DonController。增量的获取方式有两种:
-
版本对比:在分支上,第 2 次提交和第 1 次提交版本对比的增量代码,就是 DonController;第 2 次提交和刚从 master 拉取分支时版本对比的增量代码,除了 DonController 还有 WorldController;
-
分支对比:把 test 分支和 master 分支进行对比,增量代码就是 DonController 和 WorldController;
增量,就是通过版本对比或分支对比,多出来的新增加的那部分代码。覆盖率是指程序运行以后,有多少代码被执行到了,除以总的代码数算出来的,即覆盖率=执行代码行数/总代码行数。增量覆盖率是针对增量代码来计算的,也就是增量覆盖率=执行代码行数/增量代码行数。
基本实现原理

使用 JaCoCo 的 On-the-fly 模式,以 tcpserver 方式启动,将远程机器的覆盖率数据通过 TCP 通信 dump 到本地 jacoco.exec 文件,二次开发 JaCoCo 源码,实现增量覆盖率分析和输出 HTML 报告。
JaCoCo 默认覆盖率
JaCoCo 是一个开源的覆盖率工具,它的 On-the-fly 模式,无须侵入应用启动脚本,只需在 JVM 中通过-javaagent 参数指定 jar 文件启动的代理程序,代理程序在 ClassLoader 装载一个 class 前判断是否需要注入 class 文件,将统计代码插入 class,覆盖率分析就可以在 JVM 执行的过程中完成。

启动脚本示例:
java -javaagent:jacocoagent.jar -jar target/app.jar
默认情况下,JaCoCo 会在 JVM 停掉以后,生成覆盖率数据,一个 jacoco.exec 文件。
官方手册也有相关说明:

output 默认为 file:At VM termination execution data is written to the file specified in the destfile attribute。这种方式有 2 个局限,一是必须停掉应用;二是只能把覆盖率数据生成到本地文件。
output 还有另外一个选项,tcpserver:
java -javaagent:jacocoagent.jar=output=tcpserver,address=0.0.0.0,port=2014 -jar target/app.jar
address:服务端地址;
port:服务端端口;
通过这种方式启动后,会同时启动一个 TCP 服务端,使用lsof -i :2014命令查看端口进程:

然后就能通过 TCP 通信,来获取覆盖率数据,也就是所谓的”dump“。
代码片段:
@Override
public void dump(String dumpPath, String ip, int port) throws IOException {
dumpPath = FileUtil.getAbsolutePath(dumpPath);
if (!FileUtil.exist(dumpPath)) {
FileUtil.mkdir(dumpPath);
}
FileOutputStream localFile = new FileOutputStream(dumpPath + File.separator + "jacoco.exec");
ExecutionDataWriter localWriter = new ExecutionDataWriter(localFile);
SocketAddress socketAddress = new InetSocketAddress(ip, port);
Socket socket = new Socket();
try {
socket.connect(socketAddress, 10000);
RemoteControlWriter writer = new RemoteControlWriter(socket.getOutputStream());
RemoteControlReader reader = new RemoteControlReader(socket.getInputStream());
reader.setSessionInfoVisitor(localWriter);
reader.setExecutionDataVisitor(localWriter);
log.info("开始dump:{} {}", ip, port);
writer.visitDumpCommand(true, false);
if (!reader.read()) {
log.error("Socket closed unexpectedly");
throw new IOException("Socket closed unexpectedly.");
}
log.info("dump完成:{} {}", ip, port);
} catch (Exception e) {
log.info("dump失败:{}", e.getMessage());
} finally {
socket.close();
localFile.close();
}
}
dump 经过 TCP 通信,将远程机器的覆盖率数据取到本地后,生成 jacoco.exec 文件,然后可以使用 jacococli.jar 生成 HTML 报告:
java -jar jacococli.jar report ./dump/jacoco.exec --classfiles ./target/classes/ --sourcefiles ./src/main/java/ --html ./report
第 1 个参数:jacoco.exec 文件路径;
第 2 个参数:class 文件路径;
第 3 个参数:src 源码路径;
第 4 个参数:报告存放地址;
JaCoCo 也提供了 OpenApi 来生成报告。
代码片段:
dumpPath = FileUtil.getAbsolutePath(dumpPath);
classFilesPath = FileUtil.getAbsolutePath(classFilesPath);
srcPath = FileUtil.getAbsolutePath(srcPath);
reportPath = FileUtil.getAbsolutePath(reportPath);
File execFile = new File(dumpPath + File.separator + "jacoco.exec");
ExecFileLoader execFileLoader = new ExecFileLoader();
execFileLoader.load(execFile);
CoverageBuilder coverageBuilder = new CoverageBuilder();
Analyzer analyzer = new Analyzer(execFileLoader.getExecutionDataStore(), coverageBuilder);
analyzer.analyzeAll(new File(classFilesPath));
String reportTile = "报告标题";
IBundleCoverage bundleCoverage = coverageBuilder.getBundle(reportTile);
HTMLFormatter htmlFormatter = new HTMLFormatter();
IReportVisitor iReportVisitor = htmlFormatter.createVisitor(new FileMultiReportOutput(new File(reportPath)));
iReportVisitor.visitInfo(execFileLoader.getSessionInfoStore().getInfos(), execFileLoader.getExecutionDataStore().getContents());
DirectorySourceFileLocator directorySourceFileLocator = new DirectorySourceFileLocator(new File(srcPath), "utf-8", 4);
iReportVisitor.visitBundle(bundleCoverage, directorySourceFileLocator);
iReportVisitor.visitEnd();
JaCoCo 的 org.jacoco.core 和 org.jacoco.report 两个包提供了这些方法。
JaCoCo 默认只能分析全量覆盖率。
改造 JaCoCo 支持增量
改造的底层逻辑是,在分析覆盖率数据时,class 文件只选取 git diff 的文件,从而只统计差异的增量代码,实现增量覆盖率。
在 ClassProbesAdapter 类的visitMethod方法里面,有一个对方法级别的探针计算逻辑,改造它,只对提取出的每个类的新增或变更方法做解析。
代码片段:


CoverageBuilder.classInfos 是 git diff 出来的差异类。
改造 CoverageBuilder 支持分支对比和版本对比:

获取差异代码使用 jgit 和 jdt 切割到了方法粒度:

完整源码可以参考开源项目 JacocoPlus:
https://github.com/512433465/JacocoPlus
关键点
JaCoCo 分析覆盖率有时候结果不准确,通常跟这两个关键点有关:
一是 dump,dump 的频次很重要,建议每次分析时都 dump 一次,保证覆盖率数据是最新的。分布式集群会有多份覆盖率数据,可以使用 ExecFileLoader 的 load 和 save 方法将多份数据合并为一份。应用重启会丢失覆盖率数据,可以做一些备份。
二是 class,在生成报告时,会将 class 和 src 进行比对,以统计覆盖率,只有当 class 和 src 完全匹配才能得到准确的覆盖率,假如我们自己拉最新的代码进行编译得到 class,由于编译环境和版本的影响,编译出来的 class 说不定跟 src 就不匹配了,统计结果也就不会准确。最好是从应用上拉取部署的 class 来进行对比,以保证 class 和 src 的完全匹配。
JaCoCo - Java Agent https://www.jacoco.org/jacoco/trunk/doc/agent.html
JaCoCo - API Usage Examples https://www.jacoco.org/jacoco/trunk/doc/api.html
增量代码覆盖率工具 https://tech.youzan.com/yzicov/
有赞精准测试实践 https://tech.youzan.com/thanos/
Java 覆盖率 Jacoco 插桩的不同形式总结和踩坑记录 https://testerhome.com/topics/20632
jacoco 代码覆盖率使用中遇到的一些坑 https://testerhome.com/topics/16925
jacoco—增量代码覆盖率实现 https://blog.csdn.net/qq_34811445/article/details/127556617