一、覆盖率定义
作为一个测试人员,保证产品的软件质量是其工作首要目标,为了这个目标,测试人员常常会通过很多手段或工具来加以保证,覆盖率就是其中一环比较重要的环节。
我们通常会将测试覆盖率分为两个部分,即“需求覆盖率”和“代码覆盖率”。
需求覆盖:指的是测试人员对需求的了解程度,根据需求的可测试性来拆分成各个子需求点,来编写相应的测试用例,最终建立一个需求和用例的映射关系,以用例的测试结果来验证需求的实现,可以理解为黑盒覆盖。
代码覆盖:为了更加全面的覆盖,我们可能还需要理解被测程序的逻辑,需要考虑到每个函数的输入与输出,逻辑分支代码的执行情况,这个时候我们的测试执行情况就以代码覆盖率来衡量,可以理解为白盒覆盖。
以上两者完全可以相辅相成,用代码覆盖结果反向的检查需求覆盖(用例)的测试是否充分完整。
如果做覆盖率测试?我们可以借助一些网上流行的各种覆盖率工具,本章主要介绍JaCoCo这个工具。
二、JAVA覆盖率工具介绍
市场上java主要代码覆盖率工具:EMMA、JaCoCo。
总结一下个人对JaCoCo优势的理解:
(1) JaCoCo支持分支覆盖、引入了Agent模式。
(2) EMMA官网已经不维护了,JaCoCo是其团队开发的,可以理解为一个升级版。
(3) JaCoCo社区比较活跃,官网也在不断的维护更新。
我们前期使用的EMMA,也做了全量、差异覆盖率,和精准耦合也结合在了一起,但后来考虑到JaCoCo的优势,也就全部切换了过来。
2.1 JaCoCo简述
JaCoCo是一个开源的覆盖率工具(官网地址:http://www.eclemma.org/JaCoCo/),它针对的开发语言是java,其使用方法很灵活,可以嵌入到Ant、Maven中;可以作为Eclipse插件,可以使用其JavaAgent技术监控Java程序等等。
很多第三方的工具提供了对JaCoCo的集成,如sonar、Jenkins等。
JaCoCo包含了多种尺度的覆盖率计数器,包含指令级覆盖(Instructions,C0coverage),分支(Branches,C1coverage)、圈复杂度(CyclomaticComplexity)、行覆盖(Lines)、方法覆盖(non-abstract methods)、类覆盖(classes),后面会一一介绍。
我们先看看其覆盖率结果展现如下图1-1所示,方便读者先有一个大概的了解。
图1-1 覆盖率报告结果部分截图
标示绿色的为行覆盖充分,标红色的为未覆盖的行,黄色菱形的为分支部分覆盖,绿色菱形为分支完全覆盖。
通过这个报告的结果就可以知道代码真实的执行情况,便于我们分析评估结果。
2.2 JaCoCo基本概念
行覆盖率:度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。
类覆盖率:度量计算class类文件是否被执行。
分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的 分支数量。
方法覆盖率:度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。
指令覆盖:计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全 独立源码格式。
圈复杂度:在(线性)组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测 试案例没有完全覆盖到这个模块。
2.3 JaCoCo 原理
1、注入方式介绍
这个图包含了几种不同的收集覆盖率信息的方法,每种方法的实现方法都不一样,带颜色的部分是JaCoCo比较有特色的地方。
上面各个名次含义(带颜色的为JaCoCo支持):
上表JaCoCo支持的部分,再详细的解释下:
(1) JaCoCo在Byte Code时使用的ASM技术修改字节码方法,可以修改Jar文件、class文件字节码文件。
(2) JaCoCo同时支持on-the-fly和offline的两种插桩模式。
On-the-fly插桩:
JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载一个class前判断是否转换修改class文件,将统计代码插入class,测试覆盖率分析可以在JVM执行测试代码的过程中完成。
Offline模式:
在测试前先对文件进行插桩,然后生成插过桩的class或jar包,测试插过桩 的class和jar包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
On-the-fly和offline比较:
On-the-fly模式更方便简单进行代码覆盖分析,无需提前进行字节码插桩,无需考虑classpath 的设置。
存在如下情况不适合on-the-fly,需要采用offline提前对字节码插桩:
(1) 运行环境不支持java agent。
(2) 部署环境不允许设置JVM参数。
(3) 字节码需要被转换成其他的虚拟机如Android Dalvik VM。
(4) 动态修改字节码过程中和其他agent冲突。
(5) 无法自定义用户加载类。
2、JaCoCo执行最小的java版本
最小需要Java1.5
3、字节码处理方式
JaCoCo通过注入来修改和生成java字节码,使用的是ASM库。
4、java方法控制流分析
JaCoCo是如何在字节码注入的?
我们带着疑问来看下面的内容:
先举个实例,有个java方法:
编译后转换成字节码后,内容如下:
我们知道JaCoCo是字节码注入方式,它是通过一个Probe探针的方式来注入的,具体如下:
探针是字节指令集插入到java方法中,程序执行后可以被记录,它不会改变原有代码的行为。
我们看看探针前后插入比较:
颜色的部分就是探针注入的地方。
JaCoCo是根据控制流Type来采用不同的探针插入策略的。
一个用java字节码定义的java方法的控制流图可能有以下的type,每一个type连接一个源指令与目标指令,type不同探针的注入策略也会不同,如下是type定义:
探针不改变该方法的行为,但记录他们已被执行的事实,从理论上讲,可以在控制流图的每一个边插入一个探针,作为探针实现本身需要多个字节码指令,这将增加几倍的类文件的大小和执行速度。
事实上,只需要一个几个探头,根据每个方法的控制流的方法,下面说明了如何在不同的边缘类型的情况下添加额外的指令:
一个instrumented class可以用以下代码检索其探针数组实例:
JaCoCo是用一个布尔数组来实现探针,每个探针对应于该数组中的项。当以下四个字节码指令触发时探针进行输入设置为true:
JaCoCo对行探针是这样处理的,添加两行指令之间的一个额外的探针时,后续行至少包含一个方法调用。
以上是JaCoCo插桩原理,如果想深入了解,可以去看看它的源码实现。
三、JaCoCo使用方式
使用方式有很多,这里贴出了相应的参考链接,根据项目的不同可以灵活供有需要的读者去学习。
3.1 Apache Ant方式
参见 http://eclemma.org/jacoco/trunk/doc/ant.html
主要有以下几种,具体使用就不介绍了,应用宝是用的这种方式,后续有介绍。
Task coverage、Task agent、Task dump、Task merge、Task report、Task instrument
3.2 命令行方式
参见 http://www.eclemma.org/jacoco/trunk/doc/agent.html
使用方式说明:
主要放在JAVA_OPTS中,比如:
由AgentOptions的getVMArgument方法加载,各参数入AgentOptions的对应参数,为后续操作做为输入。
下面是官网的所有参数说明:
系统在jvm停止的时候会dump覆盖率信息。
关键的核心代码在这里,Agent.java在有一段代码
Runtime.getRuntime().addShutdownHook这个方法的意思就是在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁等操作。
也就是在JVM关闭的时候调用agent.shutdown(),也就是写覆盖率数据。
3.3 Apache Maven方式
参见 http://www.eclemma.org/jacoco/trunk/doc/maven.html
这种方式适合Maven的项目。
下面简单说下调用方式原理:
就拿官方的Offline Example来说吧,其部分内容如下:
注意蓝色的部分,上面的配置主要做了以下几个事情:
(1) 项目已jar包方式打包,引入junit和jacoco。
(2) Build时执行instrument、report、check。
(3) 覆盖率生成到target/jacoco.exec
我们看看他是怎么触发调用的。
在jacoco源码中:jacoco-maven-plugin\target\classes\META-INF\maven\org.jacoco\jacoco-maven-plugin目录下有个plugin-help.xml文件,它里面标明了具体的调用方式。
截出instrument这段,关键地方就是下面蓝色部分。
官网上关于参数的说明:
给出一个整理后的表格:
再给一个jacoco的maven部分的代码目录:
到这里,大家应该清楚其调用的方式了吧。
3.4 Eclipse EclDmma Plugin方式
具体步骤如下:
(1) 在Eclipse菜单中选择Help → Install New Software...
(2) 在安装弹框中输入http://update.eclemma.org/,勾选出现的版本。
(3) 核对版本,点击Next。
(4) 根据向导完成安装。
(5) 使用就不说了。
3.5 与Jekins集成
(1) 先要在jenkins上安装JaCoCo的插件,安装完成之后在job的配置项中可以增加这个选项(如图1-2):
图1-2
(2) 选择后出现(图1-3):
图1-3
第一个录入框是你的覆盖率文件(exec),第二个是class文件目录,第三个是源代码文件目录。
(3) 配置好了之后进行构建,构建完成之后job首页就会出现覆盖率的趋势图(图1-4),鼠标点击趋势图可以看到覆盖率详情(图1-5) ,包括具体覆盖率数据和源码的覆盖率情况:
图1-4 趋势图
图1-5 覆盖率详情
未完待续 :
JaCoCo原理篇就介绍到这里了,后续还有项目实践篇和踩坑篇,实践篇主要介绍下JaCoCo在实际业务中的使用情况,踩坑篇里面包含了几个当时遇到的比较棘手的问题的解决思路,有兴趣的童鞋请关注。
看了阿里开发手册 “单元测试第八条的推荐”
单元测试的基本目标:语句覆盖率达到 70% ;核心模块的语句覆盖率和分支覆盖率都
要达到 100%
Jacoco用法
首先在Maven中引入一下插件
1 <!-- 代码测试覆盖率 -->
2 <plugin>
3 <groupId>org.jacoco</groupId>
4 <artifactId>jacoco-maven-plugin</artifactId>
5 <version>0.7.8</version>
6 <executions>
7 <execution>
8 <id>prepare-agent</id>
9 <goals>
10 <goal>prepare-agent</goal>
11 </goals>
12 </execution>
13 <execution>
14 <id>report</id>
15 <phase>prepare-package</phase>
16 <goals>
17 <goal>report</goal>
18 </goals>
19 </execution>
20 </executions>
21 </plugin>
然后Maven执行 mvn clean install -Dmaven.test.failure.ignore=true
后面红色划重点,意思是:如果在单元测试中,出现了错误,那么忽略他,继续执行下去。
这样的好处是,Maven可以执行完,然后生成代码覆盖率,否则一报错,就不会生成代码覆盖率了。
Jacoco生成内容
我们可以在你的项目中 /target/site/jacoco/index.html 找到代码覆盖率
打开后如上图,红色代码没有覆盖到的代码,绿色代表已经覆盖到的代码。
Jacoco 则是使用字节码注入(Byte Code Instrumentation)的方式,使用 ASM 库在字节码中插入 Probe 探针,通过统计运行时探针的覆盖情况来统计覆盖率信息。
技术原理
On-the-fly 模式:
JVM 中通过 javaagent 参数指定特定的 jar 文件启动 Instrumentation 的代理程序,代理程序在通过 Class Loader 装载一个 class 前判断是否转换修改 class文件,将统计代码插入 class,测试覆盖率分析可以在 JVM 执行测试代码的过程中完成。
Offline 模式:
在测试前先对文件进行插桩,然后生成插过桩的 class 或 jar 包,测试插过桩的 class 和 jar 包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
存在如下情况不适合 on-the-fly,需要采用 offline 提前对字节码插桩:
- 运行环境不支持 java agent。
- 部署环境不允许设置 JVM 参数。
- 字节码需要被转换成其他的虚拟机如 Android Dalvik VM。
- 动态修改字节码过程中和其他 agent 冲突。
- 无法自定义用户加载类。
作者:LensAclrtn
覆盖率统计偏差
既然 Jacoco 是依据 class 文件进行覆盖率的统计,那么在用 EclEmma 合并会话数据时,应该保证多个会话的所测试 class 文件字节码内容是相同的,即多次测试过程中被测试 Java 类的源文件没有被修改并且重新编译过。所以在 Eclipse 中,测试用例开始执行执行后,应该保证 Testee 源文件不被改动。如果修改了被测试源文件并保存( Eclipse 会自动重新编译),请将之前的所有测试用例重新以 Coverage As 模式执行一般,否则合并后的覆盖率测试数据会有误差。
另外,由于 JaCoCo 分析统计的是编译后的 class 文件中字节码指令的执行情况。例如某源文件中有一个静态的方法 someMethod,但是在编译时 Javac 会自动为我们的类生成一个构造方法(本例中没有提供非空的构造方法),所以这个类同时有 someMethod 和一个构造方法。由于在执行静态方法过程中没有调用到构造函数,所以会显示覆盖率不是100%
作者:LensAclrtn
爱coding,也爱生活。