测试覆盖率Jacoco
介绍
Jacoco 是一个开源的覆盖率工具。Jacoco 可以嵌入到 Ant 、Maven 中,并提供了 EclEmma Eclipse 插件,也可以使用 Java Agent 技术监控 Java 程序。很多第三方的工具提供了对 Jacoco 的集成,如:Sonar、Jenkins、IDEA.
引言
对于代码覆盖率,从质量的角度来说,肯定是希望能够全部进行覆盖的,但是从实际出发,进行全覆盖也是不现实的,并且把测试覆盖作为质量目标没有任何意义,而我们应该把它作为一种发现未被测试覆盖的代码的手段。从现有的覆盖率检测工具来看,即使覆盖率到达了100%也不能代码全部分支被被覆盖到了。
代码覆盖率的意义
-
分析未覆盖部分的代码,从而反推在前期测试设计是否充分,没有覆盖到的代码是否是测试设计的盲点,为什么没有考虑到?需求/设计不够清晰,测试设计的理解有误,工程方法应用后的造成的策略性放弃等等,之后进行补充测试用例设计。
-
检测出程序中的废代码,可以逆向反推在代码设计中思维混乱点,提醒设计/开发人员理清代码逻辑关系,提升代码质量。
-
代码覆盖率高不能说明代码质量高,但是反过来看,代码覆盖率低,代码质量不会高到哪里去,可以作为测试自我审视的重要工具之一。
代码覆盖率工具
目前Java常用覆盖率工具Jacoco、Emma和Cobertura、Clover(商用)
具体见下表:
工具 | Jacoco | Emma | Cobertura |
---|---|---|---|
原理 | 使用 ASM 修改字节码 | 修改 jar 文件,class 文件字节码文件 | 基于 jcoverage,基于 asm 框架对 class 文件插桩 |
覆盖粒度 | 行,类,方法,指令,分支 | 行,类,方法,基本块,指令,无分支覆盖 | 项目,包,类,方法的语句覆盖/分支覆盖 |
插桩 | on the fly、offline | on the fly、offline | offline,把统计代码插入编译好的class文件中 |
生成结果 | 在 Tomcat 的 catalina.sh 配置 javaangent 参数,指出需要收集覆盖率的文件,shutdown 时才收集,只能使用 kill 命令关闭 Tomcat,不要使用 kill -9 | html、xml、txt,二进制格式报表 | html,xml |
缺点 | 需要源代码 | 1、需要 debug 版本,并打来 build.xml 中的 debug 编译项; 2、需要源代码,且必须与插桩的代码完全一致 | 1、不能捕获测试用例中未考虑的异常; 2、关闭服务器才能输出覆盖率信息(已有修改源代码的解决方案,定时输出结果;输出结果之前设置了 hook,会与某些服务器的 hook 冲突,web 测试中需要将 cobertura.ser 文件来回 copy |
性能 | 快 | 小巧 | 插入的字节码信息更多 |
执行方式 | maven,ant,命令行 | 命令行 | maven,ant |
Jenkins 集成 | 生成 html 报告,直接与 hudson 集成,展示报告,无趋势图 | 无法与 hudson 集成 | 有集成的插件,美观的报告,有趋势图 |
报告实时性 | 默认关闭,可以动态从 jvm dump 出数据 | 可以不关闭服务器 | 默认是在关闭服务器时才写结果 |
维护状态 | 持续更新中 | 停止维护 | 停止维护 |
Tip:Jacoco 也是
Emma 团队开发的
Jacoco 包含了多种尺度的覆盖率计数器,包含指令级(Instructions,C0 coverage),分支(Branches,C1 coverage)、圈复杂度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、类(Classes)。
-
Instructions:Jacoco 计算的最小单位就是字节码指令。指令覆盖率表明了在所有的指令中,哪些被执行过以及哪些没有被执行。这项指数完全独立于源码格式并且在任何情况下有效,不需要类文件的调试信息。
-
Branches:Jacoco 对所有的 if 和 switch 指令计算了分支覆盖率。这项指标会统计所有的分支数量,并同时支出哪些分支被执行,哪些分支没有被执行。这项指标也在任何情况都有效。异常处理不考虑在分支范围内。
在有调试信息的情况下,分支点可以被映射到源码中的每一行,并且被高亮表示。 红色钻石:无覆盖,没有分支被执行。 黄色钻石:部分覆盖,部分分支被执行。 绿色钻石:全覆盖,所有分支被执行。
-
Cyclomatic Complexity:Jacoco 为每个非抽象方法计算圈复杂度,并也会计算每个类、包、组的复杂度。根据 McCabe 1996 的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。这项参数也在任何情况下有效。
-
Lines:该项指数在有调试信息的情况下计算。
因为每一行代码可能会产生若干条字节码指令,所以我们用三种不同状态表示行覆盖率 红色背景:无覆盖,该行的所有指令均无执行。 黄色背景:部分覆盖,该行部分指令被执行。 绿色背景:全覆盖,该行所有指令被执行。
-
Methods:每一个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过。因为 Jacoco 直接对字节码进行操作,所以有些方法没有在源码显示(比如某些构造方法和由编译器自动生成的方法)也会被计入在内。
-
Classes:每个类中只要有一个方法被执行,这个类就被认定为被执行。同 5 一样,有些没有在源码声明的方法被执行,也认定该类被执行。
覆盖率使用
有2种使用方式,
- 检测单元测试的覆盖率
- 检测集成测试的覆盖率
Jacoco 收集单元测试代码覆盖率
参考https://www.jianshu.com/p/16a8ce689d60
- pom.xml 配置 plugin
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.7.201606060606</version>
<configuration>
<!--指定生成 .exec 文件的存放位置-->
<destFile>target/coverage-reports/jacoco-unit.exec</destFile>
<!--Jacoco 是根据 .exec 文件生成最终的报告,所以需指定 .exec 的存放路径-->
<dataFile>target/coverage-reports/jacoco-unit.exec</dataFile>
</configuration>
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-site</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
- 下载之后解压,直接进入工程目录,运行
mvn test
,运行项目里面的单元测试文件,接着你将看到如下图所示的文件
其中 jacoco-unit.exec 是二进制文件,就不多说了,而 index.html 就是代码覆盖率报告,如下图👇
Tip:
绿色部分:完全覆盖
黄色部分:条件覆盖
红色部分:未覆盖
针对单元测试覆盖率,官方更建议采用模式Coverage。
官网介绍:https://www.eclemma.org/userdoc/launching.html
image.png当然更多专业的应该参考官方文档
官方操作文档 https://www.jacoco.org/jacoco/trunk/doc/
image.png针对集成测试,采用代理的方式获取覆盖率
主要的解决方案是在web的运行容器tomcat里面进行设置代理,对Java字节码进行插桩。这样就可以拿到功能测试的相关覆盖率,会给测试带来量化的结果,提高测试的精准方法。
主要步骤
-
对Java字节码进行插桩,有On-The-Fly和Offine两种方式。
-
执行测试用例,收集程序执行轨迹信息,将其dump到内存。
-
数据处理器结合程序执行轨迹信息和代码结构信息分析生成代码覆盖率报告。
-
将代码覆盖率报告图形化展示出来,如html、xml等文件格式。
插桩原理
主流代码覆盖率工具都采用字节码插桩模式,通过钩子的方式来记录代码执行轨迹信息。其中字节码插桩又分为两种模式On-The-Fly和Offine。On-The-Fly模式优点在于无需修改源代码,可以在系统不停机的情况下,实时收集代码覆盖率信息。Offine模式优点在于系统启动不需要额外开启代理,但是只能在系统停机的情况下才能获取代码覆盖率。
On-The-Fly插桩 Java Agent
- JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序
- 代理程序在每装载一个class文件前判断是否已经转换修改了该文件,如果没有则需要将探针插入class文件中。
- 代码覆盖率就可以在JVM执行代码的时候实时获取。
- 典型代表:Jacoco
On-The-Fly插桩 Class Loader
- 自定义classloader实现自己的类装载策略,在类加载之前将探针插入class文件中
- 典型代表:Emma
Offine插桩
- 在测试之前先对文件进行插桩,生成插过桩的class文件或者jar包,执行插过桩的class文件或者jar包之后,会生成覆盖率信息到文件,最后统一对覆盖率信息进行处理,并生成报告。
- Offline插桩又分为两种:
- Replace:修改字节码生成新的class文件
- Inject:在原有字节码文件上进行修改
- 典型代表:Cobertura
On-The-Fly和Offine比较
- On-The-Fly模式更加方便的获取代码覆盖率,无需提前进行字节码插桩,可以实时获取代码覆盖率信息
- Offline模式适用于以下场景:
运行环境不支持java agent
部署环境不允许设置JVM参数
字节码需要被转换成其他虚拟机字节码,如Android Dalvik VM
动态修改字节码过程中和其他agent冲突
无法自定义用户加载类
实践
-
准备工作
- 下载 jacoco.zip 包
-
第一步:将下载下来的 zip 包与 Tomcat 服务放在一台机器上
-
第二步:在
[yourTomcatPath]/bin/catalina.sh
或者[yourTomcatPath]/bin/catalina.bat
添加 Jacoco 插件,指令如下
set JAVA_OPTS=%JAVA_OPTS% -javaagent:C:\Users\lenovo\Desktop\jacoco\lib\jacocoagent.jar=includes=*,output=tcpserver,address=127.0.0.1,port=4399 -Xverify:none
image.png
Tip:添加插件之前,须将的 Tomcat 服务停掉之后再添加,添加完之后,再启动 Tomcat 服务
参数说明:
1. yourPath 是放 jacocoagent.jar 文件的目录路径;那么 `jacocoagent.jar` 这个 `jar` 包的路径就是在准备工作里下载下来的 `zip` 包,解压之后的 `lib` 目录下,如:'/jacoco-0.7.9/lib/jacocoagent.jar'
2. includes 是指要收集哪些类(注意不要光写包名,最后要写.*),不写的话默认是*,会收集应用服务上所有的类,包括服务器和其他中间件的类,一般要过滤(当然如果你愿意写*也完全没有问题,如:`includes=com.*` or `includes=*`);
3. output 有 4 个值,分别是 file、tcpserver、tcpclient、mbean,默认是 file。使用 file 的方式只有在停掉应用服务的时候才能产生覆盖率文件,而使用 tcpserver 的方式可以在不停止应用服务的情况下下载覆盖率文件,后面会介绍如何使用 dump 方法来得到覆盖率文件。
4. address 是 IP 地址,IP 就是 Tomcat 服务器的机器的 IP,至于是写 `服务器本机的 IP` 还是写 `127.0.0.1` 要看情况
1) 如果是在 Tomcat 服务器上执行 `ant dump` 的话,就直接写 `address=127.0.0.1`
2) 如果执行 `ant dump` 不是在 Tomcat 服务器上执行的,就得写服务器本机的IP(切记)
5. port 是端口(端口比较随便,找个能用的端口就行,直接我为什么将端口写成 `8044`,我的想法是 `BUG 死死` 与 `8044` 挺配的,所以就用它作为端口号了)
(`address` 和 `port` 是使用 tcpserver 方式需要的 2 个参数,也是执行 ant dump 方法必须要用到的。)
6. `-Xverify:none`:这个参数是防止启动主程序异常才加的(非强制,可以不加)
检验是否部署成功:
启动tomcat, 查看对应tomcat的启动日志否带有-javaagent参数,或者在lunix服务器中ps -ef|grep tomcat
如下图,则配置成功
- 第三步:获取报告 ant dump(也是就上文中提到的,特别提醒:这里使用 ant 命令,和你的代码工程使用什么编译工具编译的没有一点关系,不要混淆)
build.xml 文件内容如下
<?xml version="1.0" encoding="UTF-8"?>
<project name="Jacoco" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">
<property name="jacocoantPath" value="[yourPath/]jacocoant.jar"/>
<property name="integrationJacocoexecPath" value="./jacoco-integration.exec"/>
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="${jacocoantPath}" />
</taskdef>
<target name="dump">
<jacoco:dump address="100.44.44.144" port="8044" reset="true" destfile="${integrationJacocoexecPath}" append="false"/>
</target>
</project>
说明:
`.exec`:二进制文件,Jacoco 就是根据这个文件生成最终的报告
`destfile`:是指生成的覆盖率文件路径
Tip:
build.xml 只需修改三个点,就可以直接拿去用
第一个修改点:补全 `jacocoant.jar` 路径。(那么 `jacocoant.jar` 在哪?对于这个问题,或许会有疑问,当然,如果细心的小伙伴就会很轻易的发现 `jacocoant.jar` 的位置,其实也就在准备工作中所下载的 `zip` 包里面,与 `jacocoagent.jar` 在同级目录 `lib` 文件夹下)
第二个修改点:修改 IP 地址(IP 须与 `catalina.sh` 中添加的一致)
第三个修改点:修改端口号(与IP一样,端口号须与 `catalina.sh` 中添加的一致)
Frequently Asked Questions:
虽然得到了集成测试的覆盖率文件,但是需要应用服务器上的类文件才能产出相应的覆盖率报告,如果类文件是其他 JVM 编译的,产出的报告覆盖率是 0%。
有 2 种方法可以得到覆盖率文件所需的 class 文件:
修改好了,那么我们来测试一下,终端进入 build.xml 所在的目录,执行:ant dump 或者 ant dump -buildfile [yourPath/]build.xml
image.png这样就已经获取到了我们需要的覆盖率文件了。
- 第四步:将代码覆盖率报告图形化展示出来
在 build.xml中添加report命令来生成报告
注意一定需要提供原始class文件和源码,而不是已经让JVM编译过的class文件,可以从仓库拉取,也可以解压war包获取,还可以在执行单元测试的时候把源码进行上传到指定文件。
<target name="report">
<delete dir="${reportfolderPath}" />
<mkdir dir="${reportfolderPath}" />
<jacoco:report>
<executiondata>
<file file="${jacocoexecPath}" />
</executiondata>
<structure name="JaCoCo Report">
<group name="Check Order related">
<classfiles>
<fileset dir="${checkOrderClasspath}">
<!-- 过滤不必要的文件 -->
<exclude name="**/R.class"/>
<exclude name="**/R$*.class"/>
<exclude name="**/*$ViewInjector*.*"/>
<exclude name="**/BuildConfig.*"/>
<exclude name="**/Manifest*.*"/>
</fileset>
</classfiles>
<sourcefiles encoding="UTF-8">
<fileset dir="${checkOrderSrcPath}" />
</sourcefiles>
</group>
</structure>
<html destdir="${reportfolderPath}" encoding="UTF-8" />
<csv destfile="${reportfolderPath}/coverage-report.csv" encoding="UTF-8"/>
<xml destfile="${reportfolderPath}/coverage-report.xml" encoding="UTF-8"/>
</jacoco:report>
</target>
一般来说,获取覆盖率文件和对应的class文件、源码,会一同存放在远程服务器上,通过Jenkins关联把它们一起拉取下来,生成相应的报告。
在本地生成相应的报告(完整版)
<?xml version="1.0" encoding="UTF-8"?>
<project name="Jacoco" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">
<!--Jacoco 的安装路径-->
<property name="jacocoantPath" value="[yourPath/]jacocoant.jar"/>
<!--最终生成 .exec 文件的路径,Jacoco 就是根据这个文件生成最终的报告的-->
<property name="jacocoexecPath" value="[yourPath/]jacoco.exec"/>
<!--生成覆盖率报告的路径-->
<property name="reportfolderPath" value="[storageReportPath]"/>
<!--远程 Tomcat 服务的 ip 地址-->
<property name="server_ip" value="100.44.44.144"/>
<!--前面配置的远程 Tomcat 服务打开的端口,要跟上面配置的一样-->
<property name="server_port" value="8044"/>
<!--源代码路径-->
<property name="checkOrderSrcPath" value="[srcPath]" />
<!--.class 文件路径-->
<property name="checkOrderClasspath" value="[classPath]" />
<!--让 ant 知道去哪儿找 Jacoco-->
<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
<classpath path="${jacocoantPath}" />
</taskdef>
<!--dump 任务:
根据前面配置的 ip 地址,和端口号,
访问目标 Tomcat 服务,并生成 .exec 文件。-->
<target name="dump">
<jacoco:dump address="${server_ip}" reset="false" destfile="${jacocoexecPath}" port="${server_port}" append="true"/>
</target>
<!--jacoco 任务:
根据前面配置的源代码路径和 .class 文件路径,
根据 dump 后,生成的 .exec 文件,生成最终的 html 覆盖率报告。-->
<target name="report">
<delete dir="${reportfolderPath}" />
<mkdir dir="${reportfolderPath}" />
<jacoco:report>
<executiondata>
<file file="${jacocoexecPath}" />
</executiondata>
<structure name="JaCoCo Report">
<group name="Check Order related">
<classfiles>
<fileset dir="${checkOrderClasspath}">
<!-- 过滤不必要的文件 -->
<exclude name="**/R.class"/>
<exclude name="**/R$*.class"/>
<exclude name="**/*$ViewInjector*.*"/>
<exclude name="**/BuildConfig.*"/>
<exclude name="**/Manifest*.*"/>
</fileset>
</classfiles>
<sourcefiles encoding="UTF-8">
<fileset dir="${checkOrderSrcPath}" />
</sourcefiles>
</group>
</structure>
<html destdir="${reportfolderPath}" encoding="UTF-8" />
<csv destfile="${reportfolderPath}/coverage-report.csv" encoding="UTF-8"/>
<xml destfile="${reportfolderPath}/coverage-report.xml" encoding="UTF-8"/>
</jacoco:report>
</target>
</project>
集成测试覆盖率和SonarQube、 Jenkins
Jenkins + Jacoco 持续集成代码覆盖率主要参考
SonarQube & SonarQube Scanner +Jacoco测试覆盖率
image获取代码覆盖率报告之后,还结合git获取的本次代码变动信息,得到测试用例覆盖的变动文件的测试覆盖率统计信息。来分析是否有由于测试用例设计遗漏导致的代码没有覆盖或者是开发的无效代码导致该代码无法被覆盖,如果测试用例设计有所遗漏,可以对照的增加相应的用例;如果是无效代码可以删除。
总结
- 代码覆盖率统计是用来发现没有被测试覆盖的代码
- 代码覆盖率统计不能完全用来衡量代码质量