Jacoco Code Coverage

Java Jacoco Ant Maven

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

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

  • 市场上主要代码覆盖率工具:
    • Emma
    • Cobertura
    • Jacoco
    • 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 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,append=true -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. append 表示覆盖率数据的追加方式,默认为 true。客户端在执行 dump 操作时,如果该 exec 覆盖率文件已存在,那么该轮的覆盖率数据会直接在文本末尾进行追加,因此会导致覆盖率数据文件越来越大。如果改为 false,则客户端执行 dump 操作时会直接清空原覆盖率文件的内容,保证该覆盖率文件只有该轮的覆盖率数据。
       7. `-Xverify:none`:这个参数是防止启动主程序异常才加的(非强制,可以不加)
    
启动 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

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,接着你将看到如下图所示的文件
    image.png

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

jacoco.xml
report
report
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

如果在阅读或者实践的过程中遇到什么问题,欢迎在下方评论

推荐阅读更多精彩内容

  • 软件的持续集成工具之一,易上手,功能强大,话不多说,干货奉上。我的博客地址:http://blog.lzoro.c...
    格子Lin阅读 4,527评论 16 37
  • 白盒测试又称结构测试、透明盒测试、逻辑驱动测试或基于代码的测试。白盒测试是一种测试用例设计方法,盒子指的是...
    yongliu1229阅读 508评论 0 4
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 122,439评论 15 533
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 67,159评论 12 114
  • 古诗漫游指南 003 清明 (唐)杜牧 清明时节雨纷纷,路上行人欲断魂。借问酒家何处有?牧童遥指杏花村。 乌青致敬...
    乌青阅读 43评论 0 1