作者:vivo 互联网服务器团队 - Xu Shen
本文次要介绍 vivo 外部研发平台应用 JaCoCo 实现测试覆盖率的实际,包含 JaCoCo 原理介绍以及在实际过程中遇到的新增代码覆盖率统计问题和频繁公布导致覆盖率失落问题的解决办法。
一、为什么须要测试覆盖率
1.1 在日常研发过程中,常常发现一些问题
- 测试案例的设计凭教训,当研发一个新性能时,常常对测试场景估计不足,到上线后发现 bug;
- 开发常常做一些需要之外的代码变更(代码小范畴内重构或在开发过程中发现小缺点顺手改掉),导致测试工作无奈测试到对应的场景,引起线上问题;
- 对测试成果无奈量化考核,导致测试工作的品质无奈进一步晋升。
1.2. 有没有技术手段可能尽可能的防止下面的问题呢?
在业内曾经在广泛应用代码覆盖率来晋升测试品质,那什么是代码覆盖率?
代码覆盖率是软件测试中的一种度量,形容程序中源代码被测试的比例和水平,所得比例称为 代码覆盖率 。
代码覆盖率指标通常蕴含上面几类:
- 函数 / 办法覆盖率:函数 / 办法中有多少被调用到
- 分支覆盖率:有多少控制结构的分支(例如 if 语句)被执行
- 条件覆盖率:有多少布尔子表达式被测试为真值和假值
- 行覆盖率:有多少行的源代码被测试过
1.3 在应用测试覆盖率的过程中,常常发现的场景
- if/else 语句中 ,if{} 内的代码被笼罩到,else{}内的代码没有被笼罩到,能够得出局部分支场景没有测试到;
- try/catch 语句中 ,try{} 内的代码被笼罩到,catch{}内的代码没有被笼罩到,能够得出异样场景没有测试到;
- if (条件 1 || 条件 2 || 条件 3)语句中,条件 1 被笼罩到,条件 2 和条件 3 没有被笼罩到,能够得出局部条件场景没有测试到;
测试人员对代码覆盖率的指标正确应用,能无效晋升测试的品质,进而晋升版本的上线品质。
二、JaCoCo 在测试覆盖率场景中的应用
2.1 JaCoCo 介绍
以后支流的代码覆盖率工具:
C/C++→Gcov,Java→JaCoCo,JavaScript→ Istanbul。
思考到服务器端次要是 Java 语言,所以 CICD 平台优先应用 JaCoCo 来反对 Java 语言的代码覆盖率统计能力。
通过 JaCoCo 官网,咱们能够看到 JaCoCo 的使命是为 Java VM 的环境中的代码笼罩剖析提供规范技术。重点是提供一个轻量级、灵便且有据可查的库,用于与各种构建和开发工具集成。
2.2 JaCoCo 长处
- JaCoCo 反对指令(C0)、分支(C1)、行、办法、类和圈复杂度等多维度的笼罩剖析;
- 基于 Java 字节码,也能够在没有源文件的状况下工作;
- 性能良好,运行时开销很小,尤其是对于大型项目;
- 比拟残缺的 API,很不便与其余工具进行集成;
- 近程协定和 JMX 管制可在任何工夫点从代理申请执行数据下载。
2.3 JaCoCo 原理
次要来自于 JaCoCo 官方网站
JaCoCo 反对几种不同的办法来收集笼罩信息,对于每种办法,由不同技术实现的,下图橙色门路局部是 JaCoCo 举荐应用的形式,即通过 On-The-Fly 形式收集覆盖率信息:
通过上图咱们晓得,JaCoCo 是通过对 Java 字节码(Byte Code)插入探针的形式来收集覆盖率信息的,探针是能够插入现有指令之间的附加指令。它们不会扭转办法的行为,但会记录它们已被执行的事实。
上面以一段简略的 程序为例进行阐明:
这段代码通过 Java 编译当前转化为以下字节码:
因为 Java 字节码指令的线性序列,控制流是通过条件或无条件指令实现跳转的,跳转指标在技术上是绝对于指标指令的偏移量。这个跟大学学习的汇编指令的跳转形式相似,为了更好的可读性,应用符号标签 (L1,L2) 代替理论的指令地址。
上图中橙色的局部为插入的探针,实践上咱们能够在控制流图的每个边缘插入一个探针,因为探针实现自身须要一些字节码指令,这将会使类文件的大小减少数倍;侥幸的是,这不是必须的,实际上咱们只须要依据办法的控制流为每个办法插入几个探针。例如,没有任何分支的办法只须要一个探针。
如果曾经执行了探测,咱们就晓得相应的边曾经被拜访过。从这条边咱们能够得出结论到其余后面的节点和边:
- 如果一条边被拜访过,咱们就晓得这条边的源节点曾经被执行了;
- 如果一个节点曾经被执行并且该节点是只有一条边的指标,咱们晓得这条边曾经被拜访过。
如果咱们在正确的地位有探针,递归地利用这些规定能够确定办法的所有指令的执行状态,探针只是须要在控制流边缘插入的一小段附加指令。
三、CICD 平台对于测试覆盖率的解决方案
通过上面对 JaCoCo 原理的介绍,联合咱们公司外部的研发流程,在 CICD 平台对代码覆盖率性能的设计如下:
从下面 CICD 平台对测试覆盖率的设计图,大略能够看进去,整个过程蕴含三个阶段
3.1 测试前
测试前由测试人员(开发人员 / 运维人员)在流水线上开启测试覆盖率性能,在流水线执行公布时,会在测试环境上下载 JaCoCo Agent 包,并在 Java 过程启动时配置 JavaAgent 参数;
在过程启动过程或启动之后,有 class 文件被加载时被 Agent 拦挡,对 class 文件进行插桩解决,在必要的门路下插入探针(插入探针的原理在上一节曾经介绍)。
3.2 测试中
在测试过程中,测试人员在测试环境执行测试案例(手动执行或自动化脚本),被调用到的代码会被探针记录下来,探针数据保留在 Java 过程的内存中。
3.3 测试后
测试人员能够屡次公布测试环境,针对同一个分支的代码,能够合并屡次测试的后果数据,造成全量的覆盖率数据;
在测试完结后,CICD 平台通过 JaCoCo 的 API,手动 / 主动下载(dump)覆盖率数据,合并(merge)历史覆盖率数据,生成测试覆盖率报告;
测试人员依据测试覆盖率报告的后果,查看测试脱漏的场景,进行补充测试,预先总结脱漏的起因,进步测试效率。
四、在实际过程中遇到的问题及解决办法
测试覆盖率在上线运行一段时间后,在实际过程中发现了一些问题,总结为以下几点:
4.1 在不同机器编译会导致 classid 不统一的问题
在实际过程中,常常遇到这样一个问题,用户反馈并确认案例曾经失常执行,然而生成的报告显示未笼罩,通过考察发现在测试环境中的 class 和生成报告时的 class 不统一导致的。
在 JaCoCo 外部,覆盖率数据是以 classid 作为 key 来存储的,classid 是依据 class 的字节码 hash 算法得进去的,看 JaCoCo 源码中对于 classid 的算法如下:
呈现不统一的状况包含:
- 公布时编译的机器和生成报告的机器环境上有差别,比方操作系统版本、JDK 版本等,导致编译的 class 不统一;
- 公布时编译的代码版本与生成报告时的代码版本有差别,导致编译的 class 不统一。
要解决下面环境的问题,须要放弃在测试覆盖率过程中编译的机器环境保持一致,或者做到只编译一次,应用同一份 class 文件,思考到存储空间的问题,vivo 采纳放弃环境统一的方法来解决。
对于第二种状况,常见于采纳麻利研发的团队,在一个版本中按性能点转测,常常导致测试在测试过程中,源代码曾经产生了批改,生成报告时代码版本和公布时的代码版本曾经不统一,这种状况比较复杂,咱们在上面会介绍。
4.2 在研发过程中更加关注增量代码的覆盖率
在咱们日常的研发流动中,对于全量代码更多应用自动化脚本来回归,而新研发的性能次要体现为增量代码,对于增量代码的覆盖率状况更加关注,JaCoCo 自身不反对增量代码的覆盖率。
对于这个问题网上也有不少解决方案,根本都是基于 git 的版本差别,在生成报告时过滤掉没有差别的类,造成两份覆盖率报告,一份是全量代码覆盖率报告,一份是增量代码覆盖率报告,而咱们更心愿在一份覆盖率报告中出现增量代码和全量代码的笼罩状况,联合代码在全量报告中的笼罩路径分析脱漏的场景,同时能在报告中标注增量代码和增量代码的笼罩状况,冀望的成果如下图所示:
为了达到上述成果,须要几个革新步骤:
- 计算出以后代码分支的变动状况,须要准确到代码行
- 革新 JaCoCo 计算逻辑,针对增量代码独自统计覆盖率指标值
- 革新 JaCoCo 报告格局,在报告中兼容全量代码和增量代码的笼罩状况
对于计算代码分支的变动状况,放弃 GitLab 提供的代码比对性能来获取不同版本之前的差别信息,如果版本之间差别太多的话,常常产生 GitLab 的 API 接口调用超时;
并且 GitLab 的比对性能无奈满足定制场景,比方一行代码仅仅因为格式化被辨认为变更代码等等,采纳借助 Linux 自带的 diff 命令,实现代码差别比对的能力:
对于革新 JaCoCo 计算逻辑,减少针对增量代码的覆盖率指标统计,在 CoverageNodeImpl 类中减少新的 Counter,用于统计新增类、办法、行、指令覆盖率指标;在 SourceNodeImple 类中 increment 办法中减少新增代码行的统计逻辑。
4.3 重谈对于 classid 的问题
在下面曾经谈到对于 classid 的问题,如果是环境问题是比拟好解决,然而当初互联网团队根本都应用麻利模式,根本不太可能等开发工作全副实现再转测,这样必然会导致最新的覆盖率报告,会呈现以类为单元的覆盖率数据失落,须要测试人员来回反复的执行测试案例,否则测试覆盖率数据不会很难看。
既然晓得问题所在,那有没有方法解决呢?是不是能够间接找到以前的 classid,把以前的 classid 对应的探针数据复制到以后的 classid 下就能够?当然是不行的,因为源代码产生变动,导致探针的数量发生变化,会呈现上面的状况:
或者这样
呈现这样的状况,会无奈判断具体哪些探针是新增的或者删除的;即便呈现前后探针统一的状况,也有可能因为代码批改,探针地位发生变化:
那么这个问题是否就无解了呢?这里给出一个大略思路,当初的覆盖率数据是以类为单位存储的,咱们能够批改存储的粒度,细化到办法级别,这样能够保留一个类的大部分探针数据,这样如果只是批改一个办法的话,那么其余办法的测试数据能够持续保留,只须要从新测试这个办法就行,这样能够无效的升高测试人员对整个类的所有计划反复测试的状况。
五、总结
对于测试覆盖率性能,有没有给测试的品质带来晋升,答案是不言而喻的。
当然也因为下面提到的问题,给测试人员带了些麻烦,为了晋升测试覆盖率数据,导致测试人员对同一个性能反复屡次测试;同时也给测试人员带来了益处,很多测试人员在面对测试覆盖率指标严格要求下,被迫去看代码的实现逻辑,晋升了本人业务水平和浏览代码的程度,甚至呈现测试人员和开发人员当面对质,对于代码逻辑是否正当的场景。
最初,测试覆盖率不是掂量测试品质的唯一标准,要正当利用测试覆盖率来晋升测试品质。