jacoco增量代码覆盖率
作者:兰绿,荣荣
文章结构
- 背景
- Jacoco简介
- Jacoco 增量代码覆盖率设计方案
- Jacoco增量代码覆盖率+持续交付
- 总结
一、背景
- 需求测试过程中,测试主要依靠需求及一些测试经验来主观保证质量。为了解决测试度量不清晰的问题,测试用jacoco从代码层面衡量测试覆盖率。分析未覆盖部分代码,从而反推前期测试设计是否重复,没有覆盖到的代码是否是测试设计的盲点。代码覆盖率可以作为测试自我审视的重要工具之一。
- jacoco代码覆盖率统计的是全量代码覆盖率,报告冗余,影响我们对报告的分析和查看。为了更精准衡量测试范围和评估影响面,我们做了改造,使jacoco报告计算增量代码覆盖率
- 功能测试,接口自动化测试,单元测试都能计算到覆盖率里面。支持通过悟空持续集成发布和预发环境手动部署两种场景下的覆盖率收集。
整体交互流程图
:
覆盖率结果:
二、JaCoCo简介
JaCoCo是一个开源的覆盖率工具(官网地址:https://www.eclemma.org/jacoco/),针对的语言为java
工作步骤:
- 对Java字节码进行插桩,有on-the-fly和offline两种方式
- 执行测试用例,收集程序执行轨迹信息,支持通过dump讲操作记录从服务端传输到本地。
- 数据处理器结合程序执行轨迹信息和代码结构信息分析生成代码覆盖率报告
- 结合源码和编译后的文件,可以将代码覆盖率报告图形化展示出来,如html,xml等文件格式
经过比较,我们选择的插桩模式是on-the-fly模式。该模式无需提前进行字节码插桩,只需要JAVA_OPTS中增加-javaagent参数,该参数会被AgentOptions的getVMArgument方法加载。参数重制定jacocoAgent.jar文件,就可以在程序启动时启动Instrumentation的代理程序,代理程序再通过Class Loader装载class前判断是否转换修改class文件将统计代码插入class
三、JaCoCo增量代码覆盖率设计方案
JaCoCo增量代码覆盖率设计方案是基于JaCoCo做相应改造,生成我们所需要的覆盖率数据。这里面主要需要解决的点在于获取增量代码并解析生成覆盖率上。
改造可以拆分成以下几个步骤:
- 获取测试完成后的exec文件(二进制文件,里面欧探针的覆盖执行信息)
- 获取基线提交和被测提交之间的差异代码
- 通过指定代码仓库名(project.key)和开发分支名(branch),解析开发分支和master之间的差异(数据需要精确到方法为度)
- 改造JaCoCo,是它支持仅对差异代码生成覆盖率报告
具体实现
1. 获取增量数据
这部分涉及到对git的操作和对java文件的语法解析,主要用JGit和JavaPaser实现:JGit是一个用于操作git的Java库,支持使用代码操作git,支持我们获得指定分支和master之间的差异,这里有一篇非常详细的JGit介绍,感兴趣的同学可以自行查阅http://qinghua.github.io/jgit/;JavaPaser是一个开源的Java语法解析库https://github.com/javaparser,可以将java源码解析为一颗语法树,分析语法树可以获得Java代码中的类,方法和方法入参等。
部分代码片段如下:
private static List<DiffResult> getDiffResult (Repository repository, String projectName, String oldCommitSha, String newCommitSha) throws IOException, GitAPIException {
List<DiffResult> results = new ArrayList<>();
List<DiffEntry> list = diff4CommitOfJava(repository.getDirectory().getPath(),
oldCommitSha,
newCommitSha);
list.stream()
//过滤,只取add和modify的内容
.filter(diffEntry -> (DiffEntry.ChangeType.ADD == diffEntry.getChangeType() ||
DiffEntry.ChangeType.MODIFY == diffEntry.getChangeType()))
.forEach(diffEntry -> {
DiffResult diffResult = new DiffResult();
HashMap<String,List<Method>> changedMethods = MethodDiff.methodDiffInClass(BlobUtils.getContent(repository,ObjectId.fromString(oldCommitSha),diffEntry.getOldPath())
, BlobUtils.getContent(repository,ObjectId.fromString(newCommitSha),diffEntry.getNewPath()));
diffResult.setPath(diffEntry.getNewPath());
diffResult.setClassName(diffEntry.getNewPath().substring(diffEntry.getNewPath().lastIndexOf("/")+1,diffEntry.getNewPath().length()));
diffResult.setEntryType(diffEntry.getChangeType()