目录
背景介绍
工具介绍
精卫覆盖率统计实现原理
步骤点描述
应用机器部署jacocoagent.jar
配置应用环境及用例
用例执行
获取exec二进制文件
获取源码及差异文件
获取应用编译包
覆盖率统计
技术架构
实现详细解读
覆盖率触发方式
构建机制
初始化覆盖率资源文件
统计覆盖率信息
界面效果展示
代码覆盖率的价值
系统特性
后期规划
总结
背景介绍
作为一名测试开发工程师,工作首要的两个目标就是测试质量和测试效率的双向提升。我们先说下如何保证被测的产品的质量呢?为了提升测试质量的目标,测试人员会通过一些手段或者工具来加以保证,而代码的覆盖率就是其中比较重要的一部分。基于目前质量部的测试现状,我们自研开发了一款代码覆盖率统计的工具,来辅助大家的日常测试,发现测试过程中漏测的代码,来补充测试场景,从而提升测试的质量及测试范围
工具介绍
Jacoco 是一个开源的 Java 代码覆盖率工具,Jacoco 可以嵌入到 Ant 、Maven 中,并提供了 EclEmma Eclipse 插件,也可以使用 JavaAgent 技术监控 Java 程序。很多第三方的工具提供了对 Jacoco 的集成,如 sonar、Jenkins 等。关于 Jacoco 的注入原理以及注入方式,网上有很多资料,这里不过多赘述。在这里我们主要介绍精卫系统是如何进行代码覆盖流程统计的,为了不让研发修改自己的服务代码,我们选择的是 On-the-fly 模式。无需提前插桩,无需考虑classpath设置问题,只需在 JVM 中通过 -jacocoagent 参数指定 jar 文件,启动 Instrumentation 的代理程序,代理程序在通过 Class Loader 装载一个 class 前判断是否需要注入 class 文件,将统计代码插入 class ,测试覆盖率分析就可以在 JVM 执行测试的过程中完成,再获取到.exec文件即可。
下面截图来自于网络,可以更快的了解jacoco代码覆盖率的获取方式
Jacoco详细介绍请见:https://www.jacoco.org/jacoco/trunk/doc/mission.html
精卫覆盖率统计实现原理
步骤点描述
1,应用机器部署jacocoagent.jar
测试人员可以通过到家运维平台,将jacocoagent.jar包部署到指定服务的机上,并且会将jacocoagent服务的启动参数配置到环境变量中,这样每次服务启动后jacocoagent服务也会一同被启动。
jacocoagent代理服务启动命令参数为:
-javaagent:/export/jacoco/jacocoagent.jar=includes=*,output=tcpserver,port=port,address=ip,append=true -Xverify:none
(命令参数中标红的部分是需要需改的)
每个参数含义请见下图:
具体参见:https://www.eclemma.org/jacoco/trunk/doc/agent.html
遇到的问题:
要确保jacocoagent.jar文件是否已经同步到服务机器的目录下,如果没有的话连带着应用服务也会启动失败,提示未找到jacocoagent.jar文件
启动参数中Includes是配置要执行分析覆盖率文件的目录,这个目录要是配置的不正确会影响界面展示的覆盖率信息
Java服务的启动脚本中,要引用“$JAVA_OPTS”JVM运行参数的,如果没有配置会不能引用服务机器上面的环境变量,导致jacocoagent代理服务不能启动
2,配置应用环境及用例
测试人员在精卫测试平台中配置环境信息,以及获取代码覆盖需要的配置参数,其中包括应用服务部署的分组名称、代码库分支和需要统计覆盖率的jar包,最后添加测试需要的回归用例
3,用例执行
测试人员就可以开始测试了,既可以在平台上面进行快速回归,也可以通过工具进行接口的测试
4,获取exec二进制文件
从应用服务的机上面获取exec二进制文件,jacoco就是根据这个文件生成最终的覆盖率信息的,exec文件后面会详细说明
5,获取源码及差异文件
从Git代码库中获取指定分支的源码信息,获取源码后会将当前分支与线上分支间做个代码差异的处理,通过差异信息与分支源码文件进行数据融合,数据融合后面有介绍
6,获取应用编译包
从应用部署平台获取到指定分支部署后class文件目录,获取覆盖率是需要源码编译后的class文件的
7,覆盖率统计
测试人员即可在精卫上面通过任务的触发,待任务分析完成之后,即可在代码覆盖率界面中查看应用系统的代码覆盖率信息了
技术架构
为了可以更好地理解,先简单的介绍下,上图大致可以分成四个步骤:
第一步:统计覆盖率任务触发
通过精卫平台创建任务,持久化到任务队列表中,worker扫描到需要处理的任务后,进行任务的过滤,当有重复的任务只保留最新的任务,之后通过多线程进行任务的执行
第二步:初始化覆盖率资源文件
任务分配执行后,会通过三个线程分别获取.exec文件、class文件和源码文件,实现过程是调用应用部署平台和Git代码库平台的开放接口,获取指定的文件保存到指定文件目录下
第三步:处理覆盖率资源文件
主要是对已经初始化的资源文件进行处理,将分支的源码文件线上代码文件进行差异化对比(diff代码),将差异化的文件件信息在与源码文件进行融合,主要标注出类文件中行的修改范围。之后将获取的三件套文件,统计生成代码覆盖率信息文件。持久化覆盖率信息和含有覆盖率的源码文件
第四步:前端界面渲染
测试人员通过前端界面,选择查看覆盖率维度,即可展示本次测试中实际的代码覆盖情况
实现详细解读
覆盖率触发方式
精卫系统是通过应用维度进行管理的,只能有应用权限的成员才能进行应用的操作,应用下可以有多个研发或者测试以及产品成员,每个测试成员对应用用例数量贡献的数量是不等的,所以精卫获取用例的覆盖率信息没有按照个人用例统计,而是按照应用维度进行统计,即执行全部回归用例。提测之后,需要测试人员进行回归测试时,只需在创建回归任务时,任务模式选择“全量”任务,即可在全部回归用例执行完成后统计应用代码覆盖率信息
构建机制
任务构建机制,精卫是采用通过创建不同类型的任务进行触发,任务的类型分为“全量任务和自定义”,当创建全量任务时,会将应用所有用例类型为回归的用例全部执行一遍,执行完之后系统即可进行覆盖率统计的操作
初始化覆盖率资源文件三件套
统计代码覆盖率,必要通过三种类型的文件才能获取服务的覆盖率信息,分别是exec文件、项目编译后的class文件和项目分支的源码文件。有的项目编译文件和源码文件还是比较大的,考虑到性能问题精卫分别启动了三个线程进行文件的获取。服务每次重新部署后,覆盖率信息就会被初始化,这样就确保每次应用部署后测试得到的覆盖率信息是完整的。
下面分别介绍下三个类型文件作用以及如何获取
.exec二进制文件
该文件是个二进制文件,里面统计了所有探针的覆盖执行信息,并且记录了代码覆盖情况,jacocoagent代理服务启动后,就会在代码中插入探针,每个探测指针都是一个布尔变量,true表示已执行,false表示未执行,程序运行时通过改变指针的结果来检测代码的执行情况,不会改变源码的执行行为。
如下图所示:
精卫是通过Socket获取的,通过与应用部署平台进行打通,调用open api接口获取IP地址列表,通过IP地址下载应用机器上的.exec文件,保存在指定的目录下,待生成覆盖率时使用
核心代码:
class编译文件
Jacoco使用一系列的不同的计数器来做覆盖率的度量计算。所有这些计数器都是从java的class文件中获取信息,这些class文件可以(可选)包含调试的信息在里面。即使在没有源码的情况下,这种方法也可以实时有效地对应用程序进行度量和分析。在大部分情况下,收集到的信息可以映射到源码,可视化到每一行代码的粒度。
编译文件是调用应用部署平台open api 接口,获取到分支部署后的zip包,解压保存在指定的目录下,待生成覆盖率时使用
分支源码文件
统计服务覆盖率信息需要源码文件,同时我们还将源码做持久化操作,目的就是在统计覆盖率信息后对源码文件进行类和行的一些处理,例如类文件是否为新增或者修改、每行的覆盖情况等…
通过与Git代码库平台打通,调用open api接口获取到分支源码文件,保存在指定的目录下,待生成覆盖率时使用
统计覆盖率信息
在获取覆盖率结果文件之前,还需处理两件事情
获取分支代码与线上代码差异信息
获取差异文件信息主要是为了获取到增量代码的覆盖率信息,获取差异文件信息是通过jgit来实现的,通过Git代码库平台获取到部署的分支代码和线上源码文件,通过两个分支间对比获取到差异化文件。对比方式我们采用多线程方式处理,提升了性能问题。在对缓存下来的源码信息标注差异信息,为之后与覆盖率数据融合使用
核心代码:
遇到的问题及处理:
在使用eclipse.jdt过程中,由于jgit版本升级,不支持jdk1.8,导致各种的依赖的类方法空指针。之后通过对pom文件各种排包,才找到一系列支持jdk1.8版本的相关依赖。后续计划将jgit所涉及到的部分代码进行抽离,分装成内部工具类提供使用
多个.exec文件要做merage合并
应用服务在部署的时候是分布式部署,所有会有多台机生成了.exec文件,所有要将所有文件进行合并,这样才能保证统计的覆盖率信息准确
合并exec文件使用org.jacoco.core.tools工具
核心代码:
这两步操作都处理完成之后就可以统计代码覆盖率信息了
生成覆盖率结果文件有三种类型格式,分支为xml、html和csv,精卫采用生成xml文件格式,xml文件中含有nr, mi, ci, mb, cb几个字段,分别代表:行号,missed instructions (statements), covered instructions (statements), missed branches,covered branches。当mb > 0或者cb > 0时,该行为分支行(标注钻石),当mi == 0 并且ci > 0,该行为全覆盖(行高亮绿色),当mi > 0 并且 ci == 0,该行为未覆盖(行高亮红色),当mi > 0 并且 ci > 0,该行为部分覆盖(行首添加黄色钻石及高亮黄色),其他则行无需覆盖
统计覆盖率信息相关的功能代码大家可以下载jacoco-master源码进行查看,地址:http://www.eclemma.org/,实现方式照搬官网的代码即可,核心代码见下图:
获取覆盖率文件信息之后还需要对数据进行处理,处理成我们需要的数据对象结构,通过对xml文件的结构我们定义了一个BaseDTO数据对象,通过处理对象的信息,进行字段信息的赋值处理,其中关键的字段信息包括:方法在类中的开始行号、总行数、行覆盖率、总指令数、指令覆盖率、总分支数、分支覆盖率、代码行覆盖状态等...。再将有覆盖率信息的源码文件持久化操作,这样前端界面就可以通过获取源码和覆盖率信息进行界面效果渲染了
对象结构:
覆盖率Xml文件转数据对象,通过javax.xml.bind工具
界面效果展示
差异覆盖率维度展示:

界面解读:

指令覆盖(Instructions)
指令覆盖率表明了在所有的指令中,哪些被指令过以及哪些没有被执行。这项指数完全独立于源码格式并且在任何情况下有效,不需要类文件的调试信息
分支覆盖(Branches)
对所有的if和switch指令计算了分支覆盖率。这项指标会统计所有的分支数量,并同时支出哪些分支被执行,哪些分支没有被执行。这项指标也在任何情况都有效。异常处理不考虑在分支范围内
圈复杂度(Complexity)
Jacoco为每个非抽象方法计算圈复杂度,并也会计算每个类,包,组的复杂度。根据McCabe1996的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。这项参数也在任何情况下有效
行覆盖
该项指数在有调试信息的情况下计算。因为每一行代码可能会产生若干条字节码指令
所以我们用三种不同状态表示行覆盖率
红色背景:无覆盖,该行的所有指令均无执行
黄色背景:部分覆盖,该行部分指令被执行
绿色背景:全覆盖,该行所有指令被执行
代码覆盖率落地情况
目前精卫系统已接入282个应用服务,可统计覆盖率系统160个,平均应用服务代码覆盖率覆盖60%以上
方法覆盖率指标要求:
目前测试人员在自己所负责的测试应用中,80%方法的行覆盖率可控制在90%以上,间接的提升了测试质量,针对研发人员,部分系统陆续的通过代码圈复杂度,来降低系统代码的复杂度,从而提升代码的可读和解耦性
系统特性
通用:支持单元测试、手工测试、全量、diff覆盖率收集
无侵入:采用on-the-fly模式,不需要开发代码做任何改造,即可收集覆盖率数据
高可用:分布式架构,任务机可无限扩展,避免任务机down机或者任务过多时出现性能瓶颈
可视化:提供增量和全量两种维度展示方式,覆盖率信息准确且可读性高
后期规划
提升异常场景的覆盖率
目前精卫针对代码中的try catch异常代码段,是不能进行完全覆盖的,之后会对这块功能进行完善,通过对编译后文件进行插桩的形式,模拟实现异常场景,提升异常场景的覆盖率
代码着色数据量的问题
由于我们是对源码进行持久化的操作,类文件行数是通数组表示的([0,0,0,0,…]),有的数据量还是很大的,消耗了不少资源,后期将会对这部分功能进行优化处理
总结
在业务快速迭代的背景下,代码覆盖率已成为测试过程中重要的一环,精卫系统可方便、快捷统计java代码覆盖率信息,并为之后精准测试铺好道路。另外在说明下,代码覆盖率决不能作为测试全面的一个保证,即便是测试覆盖到了代码行,也可能受到人员的素质和能力的影响从而出现漏测的情况。我们可以认为高覆盖率的代码不一定测试的质量高,但是低覆盖率的代码质量一定不高