持续集成 linux 下 jacoco 动态统计覆盖率

孙高飞 · July 05, 2016 · Last by yangzhuo replied at January 08, 2019 · 5522 hits

背景

之前一直用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/

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 2 条回复 时间 点赞

您好,我最近也是在搞这个jacoco,但是需求是这样的,就是开发代码和测试代码是分离的,您知道该如何实现吗

大佬你好,今天在看jacoco,因为想统计一下手工测试的覆盖率,但是公司用的是docker,想问一下,有没有好的镜像使用jacoco的方法?是不是只能以挂载的形式把jacoco、源码、ant等挂载进镜像中?期待你的回复 。谢谢

xinxi 服务端代码覆盖率统计入门 中提及了此贴 20 May 14:17
需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up