背景

之前一直用 emma 统计覆盖率,但是 emma 在 06 年的时候就停止更新了。使用的时候也确实有诸多不便,对 jdk1.7 支持的比较差,不支持 jdk1.8. 虽然其实可以凑合用着。但是对比了一下 jacoco 的使用方式和生成的报告。还是决定转成 jacoco 来做。通常我们都是将覆盖率加入到持续集成中去的。所以在自动化部署环境的时候就需要把 jacoco 集成到环境中去。我不是移动端测试者,但是据说 Android 已经默认支持 jacoco 了?不过在传统的 linux 系统上,我们还是要一步步的运行 jacoco 的任务。

Jacoco 介绍

jacoco 的前身为 emma,在 06 年的时候 emma 团队发布了最后一个版本后变宣布停止维护 emma。并另起一个项目 jacoco 来继续开展代码覆盖率的工作。

jacoco 原理

再启动任何 java 程序之前,jacoco 都会动态的将 2 进制字节码插桩进入我们的应用程序。并启动一个 tcp 的服务监控代码覆盖变化。用户可以实时的动态 dump 出覆盖率数据以生成报告。

jacoco 各项覆盖率指标
覆盖率计数器
Instructions(指令覆盖率)(C0 Coverage)

Jacoco 最小的计数单元是单个 java 二进制代码指令。指令覆盖率提供了代码是否被执行的信息。这个度量完全独立源码格式,并且总是可用,即使 class 文件里面没有调试信息。
Branches(分支覆盖率)(C1 Coverage)
Jacoco 也计算分支的覆盖率,包括所有的 if 和 switch 语句。这个度量计算一个方法里面的总分支数,确定执行和不执行的分支数量。分支覆盖率总是可用的,即使 class 文件里面没有调试信息。注意异常处理是不在分支度量里面统计的。
如果 class 文件使用调试信息编译的话,产生的覆盖率可以映射到源码行并且高亮提示:

Cyclomatic Complexity(圈复杂度)

Jacoco 同样可以为每一个非抽象方法计算复杂度,最终计算出类、包和组的复杂度。根据由 McCabe1996 圈复杂度的定义是,在(线性)组合中,计算在一个方法里面所有可能路径的最小数目。所以复杂度可以作为度量单元测试是否有完全覆盖所有场景的一个依据。复杂度即使是在没有调试信息的情况下也可以计算。
圈复杂度 V(G)的正式定义是基于方法的控制流图的有向图表示:
v(G) = E – N + 2
E 是边界的数量,N 是节点的数量。Jacoco 基于下面的方程来计算复杂度,B 是分支的数量,D 是决策点的数量:
v(G) = B – D + 1
基于每个分支的被覆盖情况,Jacoco 也为每个方法计算覆盖和缺失的复杂度。缺失的复杂度同样表示测试案例没有完全覆盖到这个模块。注意 Jacoco 不将异常处理作为分支,try/catch 块也同样不增加复杂度。

lines(行覆盖率)

所有的 class 文件使用 debug 信息编译之后,就可以计算行的覆盖率信息。一行源代码是否被执行,要看这一行中是否至少有一个指令被执行。
由于实际上一行代码一般被编译成多个二进制代码指令,这样源码在高亮显示时,会显示成 3 种不同的状态:

methods(方法覆盖率)

每一个非抽象方法至少包含一个指令。一个方法是否执行取决于方法中是否有至少一个指令被执行。在 Jacoco 中,构造器和静态初始化同样会像方法一样统计。其中一些方法可能没有可以直接对应的源码,比如默认构造器或常量的初始化命令。
classes(类覆盖率)
一个方法是否执行取决于类中是否有至少一个方法被执行。注意 Jacoco 认为构造器和静态初始化都是方法。Java 的接口一般包含静态初始化,所以接口也同样被认为是可执行的类。

具体使用

首先我们需要两个 jar 包。jacocoagent 和 jacocoant。所有关于 jacoco 的包都可以去http://www.jacoco.org/jacoco/下载
jacocoagent:运行时启动 tcp 服务监控代码覆盖,dump 出覆盖率数据
jacocoant:jacoco 的任务是 ant 驱动的。所以这个包用来执行 jacoco 的任务,向 tcp 服务发送请求。

以我们的 API 模块为例。我们通过 mvn package 打出了一个 jar 包,我们看看启动服务的命令。

nohup java -javaagent:/root/jacocoagent.jar=output=tcpserver,port=8893,address=${local_ip} -jar simba-1.0.jar --spring.config.name=application-prod --prophet.mariadb.host=${mariadb_ip} --rpc.workermanager.host=${tm_ip} --rpc.taskscheduler.host=${tm_ip} hdfs.FSHost=172.27.2.11 --hdfs.FSPort=8020 --hdfs.FSUser=hdp   >simba.log 2>&1 &

可以看到启动 java 的时候我们使用-javaagent 这个参数指定了 jcocoagent.jar 为代理。后面跟了几个参数

  1. output:输出覆盖率数据的方式,tcpserver 的意思就是开启一个 tcp 服务,动态监控覆盖率数据。用这种方式可以不停止服务就能动态 dump 覆盖率数据。否则就需要 kill 掉服务才能统计覆盖率了
  2. port:tcp 服务的端口号
  3. address:tcp 服务的 ip 地址,一般是本机地址

这样启动服务之后我们就动态的监控代码覆盖的变化了。这个时候就到了 jacocoant 出马的时候。定义一个 build.xml 文件定义 ant 任务。它会向 tcp 发送 dump 和生成 report 的请求。我们看一下具体的定义

<?xml version="1.0" ?>
<project name="Lengyu" xmlns:jacoco="antlib:org.jacoco.ant" default="jacoco">
    <!--Jacoco的安装路径-->
  <property name="jacocoantPath" value="/root/files/jacocoant.jar"/>
  <!--最终生成.exec文件的路径,里面有覆盖率数据,Jacoco就是根据这个文件生成最终的报告的-->
  <property name="jacocoexecPath" value="/opt/web/simba/file/jacoco.exec"/>
    <!--生成覆盖率报告的路径-->
  <property name="reportfolderPath" value="/opt/web/simba/coverage/"/>
  <!--服务的ip地址-->
  <property name="server_ip" value="172.27.1.216"/>
  <!--前面配置的服务打开的端口,要跟jacocoagent一样-->
  <property name="server_port" value="8893"/>
  <!--源代码路径-->
  <property name="checkOrderSrcpath" value="/opt/web/simba/src/main/java/" />
  <!--.class文件路径-->
  <property name="checkOrderClasspath" value="/opt/web/simba/target/classes" />
  <!--让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}" />
                  </classfiles>
                  <sourcefiles encoding="utf-8">
                      <fileset dir="${checkOrderSrcpath}" />
                  </sourcefiles>
              </group>
          </structure>
          <html destdir="${reportfolderPath}" encoding="utf-8" />        
      </jacoco:report>
  </target>
</project>

我们运行 ant dump 就会 dump 出 exec 文件,里面有覆盖率的数据。然后运行 ant report,就会根据 exec 生成相应的覆盖率报告。注意 build.xml 的配置一定要正确。尤其是 exec 的路径和 jacoco 的 jar 包路径,tcp 服务的 ip 和 port
更多的使用方式,请参考官方文档:http://www.eclemma.org/jacoco/trunk/doc/


↙↙↙阅读原文可查看相关链接,并与作者交流