测试覆盖率 Jacoco-报告改造实践

乱舞 · 2021年01月23日 · 最后由 零星小雪 回复于 2021年03月10日 · 8369 次阅读
本帖已被设为精华帖!

1. 背景

公司这边在代码覆盖率方面,依旧使用的传统手工方式生成,配置和操作比较麻烦,对业务同学也有些门槛。正好 Q4 不算特别忙了,抽出了一些精力来开发了这个代码染色平台,帮助业务同学一站式的解决权限申请、环境配置、报告保存、报告解读、信息汇总、增量分析等方向的问题。为什么叫代码染色平台呢,其实是看到了蚂蚁在 MTSC 上实时代码染色的视频和 PPT 资料,觉着很合适,便顺势当了搬运工,把这个词借了过来。开发过程中,借鉴了论坛中很多相关文章和资料,也和朋友进行过交流,有一次 [ID:zailushang] 还把他的同事拉群一起交流沟通,再次感谢😄。所以也把自己产出的一些东西,在社区和伙伴们分享一下,如果其中的思路或细节能帮到一些人,也是再好不过了。

2.做了哪些事

① 为了做成通用平台,以便跨线团队都能用,系统集成了公司内部的各个平台,如发布平台、CI 平台、统一登录、权限控制等。主要点在于 Jacoco Agent 安装与发布平台结合、代码获取与 CI 平台结合、报告生成与存储平台结合。
② 提供了实时获取 exec 并生成报告,exec 合并,报告存取等基础功能
③ 提供了团队视角的项目、应用、环境的管理,将覆盖率报告与之关系映射,方便灵活切换项目环境查看
④ 提供了基于 git diff 的简单增量功能、基于 AST 的关联分析的高级增量功能。
⑤ 对 Jacoco 原生报告的指标统计进行了筛选展示、中文展示。把 jacoco missed 思维的统计修改成 covered。取消掉了 jacoco 的面包屑,感觉不好用。在 jacoco 项目 index 页、包 index、类 index 页增加了带饼图的统计区域,便于在测试报告中进行汇总。
⑥ 提供指定包或类的排除和包含功能

3. 平台的概要流程

注:下方图 1 展示了系统的概要流程,图 2 为系统报告页的截图, 图 3 为报告改造后的细节。


4. Jacoco Report 的生成调用流示意

对 jacoco 报告生成流程,整理了一份类图形式的简单顺序示意,如有偏差欢迎指正。其中绿色实线表示调用关系,黑色虚线表示继承关系。图示如下:

图片也更登陆https://www.processon.com/view/link/5ffbe5747d9c080e58b94417 进行查看。

觉着不错帮点个赞,//每 5 个获赞能增加 processOn 一个免费文件

我们都知道,jacoco 报告的生成入口,是官方 ReportGenerator.java (https://www.jacoco.org/jacoco/trunk/doc/examples/java/ReportGenerator.java) 类中的 create 方法,然后通过 createReport 中的 visitBundle 调用 HTMLFormatter 类的 visitBundle() 来正式开始报告的处理。再后边的详细调用顺序,可以参考上图。对应调整的代码,会在下方部分详细说。

5. 源码改造的细节

5.1 右侧统计指标项

经过分析原始的 jacoco report 统计项,发现原生的 miss% 不容易被用户接受,大家预期的都是 covered%。另外根据调研,用户对圈复杂度和指令覆盖度的指标不是特别关注。因此这里只做了三件事:
A: 删除 Instructions 及 Cxty 的指标项 B: 将 miss% 修改为 covered% C: 将英文指标修改为中文,效果如下:

源码中改动的位置大致有两处,如下:
① 生成 table 列名方法,org.jacoco.report.html.HTMLFormatter.createTable()。删除了 addMissedTotalColumns() miss 列的添加,仅用 PercentageColumn 展示改为中文的列,不再使用 BarColumn

② 生成 table 数据方法,table 底部的统计项目从 missed of count 改为 covered of count

5.2 删除 jacoco 头部面包屑、Session 链接和底部标签

此处修改的目的在于,我自己在报告左侧做了树形的节点 Tree,点击后可进行页面导航。另外当在源码目录时,常常因为代码行数过多很难翻到面包屑处,进行页面跳转。底部标签内容中没有价值数据,删除后利于页面整洁。
源码中改动的位置大致有 3 处:
① 在 org.jacoco.report.internal.html.page.ReportPage.render() 中删除 attr("onload") 和 content() 外所有代码,如下:

private void body(final HTMLElement body) throws IOException {
   body.attr("onload", getOnload());   
   //final HTMLElement navigation = body.div(Styles.BREADCRUMB);   
   //navigation.attr("id", "breadcrumb");   
   //infoLinks(navigation.span(Styles.INFO));   
   //breadcrumb(navigation, folder);   
   //body.h1().text(getLinkLabel());   
   content(body);   
   //footer(body);
}

注:ReportPage 是除了源码页面外,所有页面的公用生成类,因此修改此类的 body 方法就可以影响到汇总页、包的类列表页、类的方法列表页等
② org.jacoco.report.html.HTMLFormatter.visitBundle() 中删除或注掉 createSessionsPage(page);
③org.jacoco.report.html.HTMLFormatter.visitEnd() 中删除或注掉 sessionsPage.render();

5.3 增加统计区域

这里整洁统计区域,是为了在测试完结,编辑测试报告时,能够方便美观的汇总项目级或包级、类级的覆盖率数据,并能明确的看到应用名称、环境信息、创建时间、统计对象等。然后就有了下图:

因为要修改汇总页、包页、类页,根据上面的源码分析,这个功能也应该加到 ReportPage 上。这里大致涉及到处变化,如下:
① 欲传递项目、应用、环境、唯一时间标识等信息到 ReportPage.body()。需要从生成报告的入口 create() 方法,就传入这些参数。并生成报告的调用链的方法上增加参数传递,可以参考第 4 部分的图示。
org.jacoco.report.html.HTMLFormatter.visitBundle():

org.jacoco.report.internal.html.page.BundlePage.render(),另外和 BundlePage 一样继承于 TablePage 的类的 render() 方法全都需要增加参数传递。

② 在 ReportPage 添加展示的区域 div,在 div 中添加展示的 span、div、及 graph-div 等。这里需要提及的是,需添加元素基本都是通过 HTMLElement.elementXX() 方式创建,创建的位置是由调用方 parent.elementXX 决定的,创建顺序需要特别注意,要处理完一个元素再去 new 下个区域,这个需要自己试验来体会。下方贴出部分关键代码:
ReportPage.body() 方法开始创建区域 div,并逐步填充里边的元素

ReportPage.createTitleArea() 方法创建文字相关的展示元素和计算展示数值

ReportPage.summary() 方法初始化图表的元素和布局


③ 页面引入 echarts js,为图表做数据初始化
ReportPage.render() 方法添加 highcharts js

ReportPage.drawGraph() 方法对 highcharts 继续设置和图表渲染

6. 后续计划

应客户需求,预计在 Q1 实现两个定制功能:
① 增加针对 Pom 依赖包的覆盖率并集统计
② 增加方法执行次数统计,与包、类、方法、行指标并列展示

共收到 22 条回复 时间 点赞

直接用原生的好用吗?

干货满满,终于等到啦,哈哈

感谢分享,顺便挑个刺,
😂

MarvinWu 回复

😅 还真是 大意了,已改正

原生报告有的 RD 和 QA 看不太明白,不够直观。并且发报告时候还需要把各包层结果统计后自己调格式,再粘贴到测试报告里,不统一且有算错 (也包含故意) 的可能性,

zailushang 回复

正好这周末没啥事 抓紧写了

恒温 将本帖设为了精华贴 01月24日 13:35

已经被社区公众号采用~

想问下,你的定时刷新时怎么做到的,每隔十秒,重新 dump exec 文件,然后生成跟上一份命名一样的 report 文件进行加载?

很棒的实践

mark 一下,

很赞!

乱舞 #14 · 2021年01月25日 Author
test小生 回复

是的,覆盖方式。前端做定时器调接口,接口每次生成报告时候都删除指定项目环境下的报告,生成一个新的。如果需要永久保存的话 ,可以走保存功能,去别目录再存一份

666,我们也打算做这个,不知是否可以交流下~

乱舞 回复

还有几个问题请教下,如果用户点击到具体的某个类页面,如果实时染色的话,是否会停留在该页面?如果是版本代码变化了,新版覆盖率数据集成旧版的报告上做实时染色呢,一般来说,版本变化,我得理解是版本变化覆盖率会被清零。

乱舞 #17 · 2021年01月25日 Author
chenyouan 回复

先留言瞅瞅

乱舞 #18 · 2021年01月25日 Author
test小生 回复

按两个方向说:
① 代码版本变化了,如果没有重新部署,就是按照旧的代码进行报告生成,新代码对环境和报告无影响。我这边的代码版本是和公司的发布平台对接的,代码构建部署后,才按新代码生成报告。另外重新部署后覆盖率肯定会清零,但可以和老版本代码的 exec 合并,用新代码生成合并报告
② 实时染色页面停留这块,每刷新一次报告,都会删除旧的报告,展示全新生成的

Jacoco Agent 安装与发布平台结合这块可以细讲一下如何结合吗。不知道我们这边 APP 测试是否也可以同理应用

乱舞 #20 · 2021年02月04日 Author
Cheny 回复

主要是检测发布平台当前部署的版本是哪个,然后去到打包平台下载对应版本的源码,还有就是从部署平台拿部署 jacoco agent 的 ip 用来下载 exec。 我这边不是业务部门,没有 app 的需求,所以没有做这块。按说可以在打包编译期增加 jacoco 的统计代码织入,后期 app 运行后再从 sd 卡获取 exec。这篇文章可能对你有用:https://zhuanlan.zhihu.com/p/88317244

4楼 已删除

您好,可以贴下 Object summaryData = getSummaryNode(body); 方法代码吗。

乱舞 #23 · 2021年03月01日 Author
zhntester 回复

body 参数不重要,本质都是 super.getResultNode()

@Override
public Object getSummaryNode(HTMLElement body) throws IOException {
return super.getResultNode();
}

再做一个类似的东西,做 codediff 的报告生成,现在遇到个问题就是报告不知道生成在项目的哪个子模块下面

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册