Jacoco Code Coverage

Java Jacoco Ant Maven

近期因工作需要,需对代码覆盖率进行统计,所以这篇就当做对这段时间学习的总结。
总得来说网上找到的资料都不系统,不适合新手理解和参考,下面我就以我一个小白的亲身体验,将我
踩到的那些坑和遇到的那些疑惑记录下来
(作为一名初学者,文章中可能会有错误或者理解偏差的地方,欢迎各位批评指正)

代码覆盖率工具调研信息如下:

  • 市场上主要代码覆盖率工具:
    • Emma
    • Cobertura
    • Jacoco
    • Clover(商用)

具体见下表:

工具JacocoEmmaCobertura
原理使用 ASM 修改字节码修改 jar 文件,class 文件字节码文件基于 jcoverage,基于 asm 框架对 class 文件插桩
覆盖粒度行,类,方法,指令,分支行,类,方法,基本块,指令,无分支覆盖项目,包,类,方法的语句覆盖/分支覆盖
插桩on the fly、offlineon the fly、offlineoffline,把统计代码插入编译好的class文件中
生成结果在 Tomcat 的 catalina.sh 配置 javaangent 参数,指出需要收集覆盖率的文件,shutdown 时才收集,只能使用 kill 命令关闭 Tomcat,不要使用 kill -9html、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 Java Code Coverage Library

Jacoco 是一个开源的覆盖率工具。Jacoco 可以嵌入到 Ant 、Maven 中,并提供了 EclEmma Eclipse 插件,也可以使用 Java Agent 技术监控 Java 程序。很多第三方的工具提供了对 Jacoco 的集成,如:Sonar、Jenkins、IDEA.

Java Counters

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 一样,有些没有在源码声明的方法被执行,也认定该类被执行。

Jacoco 原理

参考资料:

  1. 浅谈代码覆盖率
  2. Jacoco 的原理
  3. Java 代码覆盖率工具 JaCoCo 原理篇

好了,废话不多说,咱们直奔主题,大家只要按照操作步骤执行就可以

Jacoco 收集集成测试代码覆盖率

什么是集成测试?
  • 准备工作

  • 第一步:将下载下来的 zip 包与 Tomcat 服务放在一台机器上

  • 第二步:在 [yourTomcatPath]/bin/catalina.sh 添加 Jacoco 插件,指令如下👇

     JAVA_OPTS="-javaagent:[yourPath/]jacocoagent.jar=includes=com.companyName.*,output=tcpserver,port=8044,address=100.44.44.144 -Xverify:none"
    

    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`:这个参数是防止启动主程序异常才加的(非强制,可以不加)
    

《Jacoco Code Coverage》

启动 Tomcat 服务之后,ps 一下,如果在 Tomcat 服务中有 jacocoagent 这个服务的话
那么恭喜你,你成功了!!!
  • 第三步:获取报告 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 文件:
1. 将应用服务部署的包(ear 或 war 或 jar)包下载下来之后解压,即可得到对应的 class 文件;
2. 在前面做单元测试之后,可以将 class 文件打成一个 zip 包,然后上传到服务器,最后在需要的时候去服务器上取。

修改好了,那么我们来测试一下,终端进入 build.xml 所在的目录,执行:ant dump 或者 ant dump -buildfile [yourPath/]build.xml

《Jacoco Code Coverage》 ant dump

成功之后,接下来就是 Jenkins 集成 jacoco 实现代码覆盖率,详见:
Jenkins + Jacoco
持续集成代码覆盖率

是不是只有上面的这一种方式呢?当然不是!
第二种方式(不推荐):
JAVA_OPTS="-javaagent:[yourPath/]jacocoagent.jar=destfile=[storagePath/]jacoco.exec
同样是加载 cataline.sh 文件中,除了获取报告的方式上面的不一样之前,其余步骤都一样

获取报告:
功能测试或者接口自动化后,需要获取报告的话,需关闭 Tomcat 获取结果文件 `jacoco.exec`,使用 kill [PID],之后到你保存的路径下就能看到 `jacoco.exec` 文件(切记不要使用 kill -9 [PID],否则不能生成结果)
不推荐这种方式的理由:如果使用这种方式的话,不好做持续集成,因为 jenkins 服务器基本上都是和部署代码的服务器分开的,所以要从远程服务器取结果的话还是选择上面的方式
Q:那现在可能又有同学会问,这个报告只能在 `Jenkins` 上面生成吗?
A:当然也可以在本地生成了,附上代码,如下👇
<?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>

Jacoco 收集单元测试代码覆盖率

  • 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>
Demo 工程下载
  • 下载之后解压,直接进入工程目录,运行 mvn test,接着你将看到如下图所示的文件
    《Jacoco Code Coverage》 image.png

其中 jacoco-unit.exec 是二进制文件,就不多说了,而 index.html 就是代码覆盖率报告,如下图👇

《Jacoco Code Coverage》 jacoco.xml
《Jacoco Code Coverage》 report
《Jacoco Code Coverage》 report
《Jacoco Code Coverage》 report

Tip:
绿色部分:完全覆盖
黄色部分:条件覆盖
红色部分:未覆盖
  • 合并集成测试代码覆盖率和单元测试代码覆盖率,build.xml 代码如下👇
<?xml version="1.0" encoding="UTF-8"?>
    <project name="Jacoco" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">
    <property name="baseDir" value="[yourExecFilePath]"/>   
    <property name="jacocoantPath" value="[yourPath/]jacocoant.jar"/>
    <property name="allJacocoexecPath" value="./jacoco-all.exec"/>
    
    <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
      <classpath path="${jacocoantPath}" />
    </taskdef>

    <target name="merge">
        <jacoco:merge destfile="${allJacocoexecPath}">
        <fileset dir="${baseDir}" includes="*.exec"/>
        </jacoco:merge>
    </target>
</project>

只要将这份 build.xml 放在代码的根目录下,执行 ant merge 就可将所有以 .exec 文件合并,重新生成名为 jacoco-all.exec 的二进制文件,当然也可以将文章中的两份 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="baseDir" value="[yourExecFilePath]"/>
    <property name="integrationJacocoexecPath" value="./jacoco-integration.exec"/>
    <property name="allJacocoexecPath" value="./jacoco-all.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>

    <target name="merge">
        <jacoco:merge destfile="${allJacocoexecPath}">
        <fileset dir="${baseDir}" includes="*.exec"/>
        </jacoco:merge>
    </target>
</project>
分别执行:
    `ant dump` & `ant merge`
          or 
    `ant dump -buildfile [yourpath/]build.xml` & `ant merge -buildfile [yourpath/]build.xml`
这样生成的代码覆盖率报告中既包含集成测试代码覆盖率,又包含单元测试代码覆盖率的报告

将 .exec 文件合并之后,参照上文中提到的 Jenkins + Jacoco 持续集成代码覆盖率 这篇文章,将它与 Jenkins 集成。当然还可以借助于 Sonar 将静态代码检查的数据与代码覆盖率同步到 SonarQube 平台,详见:SonarQube & SonarQube Scanner

如果在阅读或者实践的过程中遇到什么问题,欢迎在下方评论
    原文作者:纳爱斯
    原文地址: https://www.jianshu.com/p/16a8ce689d60
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞