Jacoco覆盖率使用总结

tags: Java

前阵子使用 Jacoco 进行代码覆盖率测试,由于项目特殊遇到了不少坑,网上搜到的教程感觉也不够全面,特此记录。
所用到的工具软件的版本信息如下

  • Jacoco 版本:0.8.0
  • Eclemma 版本:3.0.0
  • Eclipse 版本:4.3
  • JDK 版本:1.8
  • ANT 版本:1.9

1. 工具介绍

JaCoCo,即 Java Code Coverage,是一款开源的 Java 代码覆盖率统计工具。支持 Ant 、Maven、Gradle 等构建工具,支持 Jenkins、Sonar 等持续集成工具,支持 Java Agent 技术远程监控 Java 程序运行情况,支持Eclipse、IDEA等IDE,提供HTML,CSV 等格式的报表导出,轻量级实现,对外部库和系统资源的依赖性小,性能开销小。

JaCoCo 支持从 JDK1.0 版本到 JDK1.8 版本 的 Java 类文件。但是,JaCoCo 工具所需的JRE 版本最小为 1.5。另外,1.6及以上版本的测试中的类文件必须包含有效的堆栈映射帧。

覆盖率统计数据

2. 入门使用

本文将以 tcpserver 模式远程获取应用覆盖率,通过 Ant 脚本执行相关命令,在 Eclipse 上查看源码覆盖率情况。

2.1 配置部署

先从官网获取 Jacoco 的压缩包, 将其上传到你要进行覆盖率检测的应用所在的服务器上。在解压后的 lib 目录下找到 jacocoagent ,将其路径添加到 JAVA_OPTS 环境变量中(如果项目中用到了 Tomcat,也可以直接将其添加到 CATALINA_OPTS 的环境变量中,JAVA_OPTS 只是更通用而已)。

如果是 Windows 系统,将以下内容追加到 JAVA_OPTS 环境变量。

-javaagent:D:\jacoco-0.7.9\lib\jacocoagent.jar=includes=*,address=10.1.231.168,port=6300,output=tcpserver,append=true;%JAVA_OPTS%

如果是 Linux 系统,可以直接编辑 .bash_profile

export JACOCO="-javaagent:/$your_path/jacocoagent.jar=includes=com.grgbanking.*,output=tcpserver,address=11.111.1.11,port=6300,append=true"
export JAVA_OPTS="$JACOCO":"$JAVA_OPTS" 

其中常用选项的含义如下

  • javaagent: 指定 jacocoagent 的路径
  • includes: 表示只对指定包下的类进行覆盖率注入分析,默认为 *,示例中只分析 com.test 包的类。
  • output: 表示覆盖率的输出方式。在 tcpserver 模式下,Jacoco 会在客户端执行 dump 操作时将目前收集获取到的覆盖率数据统一写到指定的ip和端口。在 file 模式下,Jacoco 只会在JVM 终止的时候才将收集到的覆盖率数据写入到指定的 exec 文件里去。注意,不管是任何模式,应用运行过程中的临时覆盖率数据都是保存在服务端的内存中的,因此对于 tcpserver 模式来说,如果 JVM 不小心终止了,那么在这个覆盖率统计周期内的覆盖率数据都会丢失。还有一个 tcpclient 模式则是以客户端的形式启动,由于目前没有这个使用场景,这里不过多讨论。
  • address: 只限 tcpserver 与 tcpclient 使用,表示监听的应用服务器IP地址或主机名。可根据实际情况自由选择。
  • port: 只限 tcpserver 与 tcpclient 使用,表示监听的应用服务器的端口号,一般用默认6300即可。
  • append: 表示覆盖率数据的追加方式,默认为true。客户端在执行 dump 操作时,如果该 exec 覆盖率文件已存在,那么该轮的覆盖率数据会直接在文本末尾进行追加,因此会导致覆盖率数据文件越来越大。如果改为false,则客户端执行 dump 操作时会直接清空原覆盖率文件的内容,保证该覆盖率文件只有该轮的覆盖率数据。

修改好以后启动 Java 应用,读取 JAVA_OPTS 环境变量的信息,Jacoco 被加载进。检查下6300端口如果已监听,说明服务端 Jacoco 启动成功。

2.2 数据获取

在正常运行过程中,服务器端的 Jacoco 只是将获取的覆盖率数据保存到内存中,我们还需要在客户端上进行操作才能将覆盖率数据 dump 到客户端。

Jacoco 为我们提供了 Ant、Maven、CLI 等多种方式进行操作,其中 CLI 方式唯一的用途就是可以用来执行 execinfo 命令,这个命令是 Ant 与 Maven 所没有的,它可以将 exec 简单转成文本格式方便你查看每个类的覆盖率百分比。Maven 与 Ant 大同小异,由于项目中使用 Ant 进行构建,下文中将以 Ant 为例讲解。

在使用 Ant 脚本获取覆盖率之前,我们需要先去官网下载好 Ant,注意安装过程中要手动勾选 “添加到环境变量” 的相关选项,省得以后要自己添加。
安装好以后打开 cmd 输入ant -version,如果能显示相关的版本信息例如 “ Apache Ant(TM) version 1.9.11 compiled on March 23 2018 ”,则说明 Ant 安装成功。

虽然官方也提供了 Ant脚本,但较为简单,部分内容没有说明,因此文末会附上我在项目中使用的完整脚本。

2.3 统计分析

对于不熟悉 Java 或者对项目目录结构不了解的朋友,往往会由于源码和字节码不匹配或者路径错误导致在结合源码查看覆盖率时反复折腾,跑半天不知道生成的 exec 到底有没有统计到。这时候我们可以使用 CLI 中的 execinfo 命令,简单查看下 exec 文件中的覆盖率是否为0。
java -jar D:\jacococli.jar execinfo E:\jacoco\igaps1008.exec

这种方式只能查看 exec 文件的概况,要想结合源码查看详细的覆盖率使用情况,我们还是需要花点时间,配置好源码和字节码,这样才能在 IDE 中查看源码覆盖率。

首先需要在 Eclipse 中安装 Eclemma 插件,你可以使用 Eclipse 的 MarketPlace 在线安装,

在线安装

也可以下载离线安装包 eclemma-3.0.0.zip,分别将里面的 features 和 plugins 文件夹里的 jar 包拷贝到 Eclipse 对应的文件夹中,重启 Eclipse 后如果有显示覆盖率图标或视图就说明安装成功了。

安装后界面

接着下载项目源码并将项目导入到 Eclipse 中

项目目录结构

注意导入前取消 Eclipse 中的自动编译(即 Project - build automatically ), 然后拷贝服务器上的字节码文件到这个项目的编译输出文件夹中。例如这个项目的编译输出文件夹为根目录下bin目录,那么就把字节码文件都拷贝到这个目录下,到这里我们的项目就准备好了。

在 Eclipse 控制台 Coverage 视图窗口的空白位置,右键--Import Session,在 Coverage Session 窗口,选择第三个代理模式,Agent address 填写需要监控覆盖率的远程服务器地址。点击下一步后,选择需要查看覆盖率的源码,一般不需要勾选include binary libraries,再点击Finish即可查看覆盖率。

导入覆盖率数据
tcpserver模式

3. 注意事项

  • 官方文档才是王道
    强烈建议在使用 Jacoco 之前阅读官方文档,虽然是英文,但是内容也很简单,花1个小时大概浏览下能对 Jacoco 有个系统性的了解。这里对 Jacoco 的官方部分 FAQ 进行了简单翻译,同时加入了部分自己在使用过程中遇到的坑。

  • 源代码没有覆盖率高亮问题
    必须确保使用调试信息编译类文件以包含行号,如果使用 Ant 编译脚本,则需要检查脚本中 javac 相关部分是否没有设置 debug=true。
    源文件必须在报表生成时正确提供。即指定的源文件夹必须是定义Java包的文件夹的直接父级。

  • 覆盖率统计偏差
    既然 Jacoco 是依据 class 文件进行覆盖率的统计,那么在用 EclEmma 合并会话数据时,应该保证多个会话的所测试 class 文件字节码内容是相同的,即多次测试过程中被测试 Java 类的源文件没有被修改并且重新编译过。所以在 Eclipse 中,测试用例开始执行执行后,应该保证 Testee 源文件不被改动。如果修改了被测试源文件并保存( Eclipse 会自动重新编译),请将之前的所有测试用例重新以 Coverage As 模式执行一般,否则合并后的覆盖率测试数据会有误差。
    另外,由于 JaCoCo 分析统计的是编译后的 class 文件中字节码指令的执行情况。例如某源文件中有一个静态的方法 someMethod,但是在编译时 Javac 会自动为我们的类生成一个构造方法(本例中没有提供非空的构造方法),所以这个类同时有 someMethod 和一个构造方法。由于在执行静态方法过程中没有调用到构造函数,所以会显示覆盖率不是100%

  • Android应用使用覆盖率
    由于Android不能通过JVM停止后自动dump覆盖率数据,因此当Android应用进程不存在或停止的时候,覆盖率数据不会生成。要想获得Android应用的覆盖率,,必须使用离线插桩模式进行覆盖率分析

  • 多源文件目录
    Ant 脚本执行起来很方便,但如果要执行 report 命令则需要注意,如果该应用编译后的class 或 jar 分别在几个不同的目录下,那么就需要分别在 Ant 脚本中指定 group,同时每个 group 也都要指定源文件 sourcefiles 以及 编译后的类文件 classfiles。同样的,如果
    项目的源码存放目录也没有统一的入口,那也需要在一个 sourcefiles 中指定多个 fileset,就如本脚本中分别指定了<fileset dir="${JacocoClassPath}/lib/core"/><fileset dir="${JacocoClassPath}/project"/> 这2个 classfiles 一样。

  • Eclipse中导入覆盖率数据时出错
    如果在Eclipse的Eclemma插件中导入exec文件时弹窗,提示 “Error while loading coverage session (code 5001).”
    一般是因为eclipse 中导入的项目编译输出文件夹目录结构不合法导致,同时 class 文件必须是从服务器中获取的,不能使用 eclipse 自己的编译器编译的 class。由于 Eclipse 默认会开启自动编译,所以哪怕你没有手动编译,在你导入项目的时候 Eclipse 已经帮你编译了一次了。这里必须删掉编译后的 class 然后重新拷贝一份服务器上的 class 文件

导入失败

4. 技术原理

运行时分析 (Runtime Profilling) 技术 在 PureCoverage 中有使用,他就是通过 JVMTI 来监听 JVM 的相关事件进行覆盖率数据收集,而 Jacoco 则是使用字节码注入(Byte Code Instrumentation)的方式,使用 ASM 库在字节码中插入 Probe 探针,通过统计运行时探针的覆盖情况来统计覆盖率信息。

技术原理

On-the-fly 模式:
JVM 中通过 javaagent 参数指定特定的 jar 文件启动 Instrumentation 的代理程序,代理程序在通过 Class Loader 装载一个 class 前判断是否转换修改 class文件,将统计代码插入 class,测试覆盖率分析可以在 JVM 执行测试代码的过程中完成。

Offline 模式:
在测试前先对文件进行插桩,然后生成插过桩的 class 或 jar 包,测试插过桩的 class 和 jar 包后,会生成动态覆盖信息到文件,最后统一对覆盖信息进行处理,并生成报告。
存在如下情况不适合 on-the-fly,需要采用 offline 提前对字节码插桩:

  1. 运行环境不支持 java agent。
  2. 部署环境不允许设置 JVM 参数。
  3. 字节码需要被转换成其他的虚拟机如 Android Dalvik VM。
  4. 动态修改字节码过程中和其他 agent 冲突。
  5. 无法自定义用户加载类。

5. Ant 脚本

<?xml version="1.0" encoding="UTF-8" ?>
<project default="report" basedir="." xmlns:jacoco="antlib:org.jacoco.ant">

    <!-- 定义 Jacoco 相关变量和库路径 -->
    <property name="JacocoIP" value="192.168.22.33"/>
    <property name="JacocoPort" value="6300" />
    <property name="JacocoExec" value="./jacoco/merge-0608.exec" />
    <property name="JacocoReport" value="./jacoco/igaps-report.zip" />
    <property name="JacocoSrcPath" value="."/>
    <property name="JacocoClassPath" value="./igaps/apps"/>
    <property name="Encoding" value="UTF-8"/>
    <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
        <classpath path="E:/jacoco/lib/jacocoant.jar"/>
    </taskdef>
    
   <!--  1 获取覆盖率exec文件 -->
   <target name="dump">
      <jacoco:dump address="${JacocoIP}" port="${JacocoPort}" reset="false" append="true" destfile="${JacocoExec}"  />
   </target>
   
   <!-- 2 合并exec文件 -->
   <!-- 获取指定目录下的所有 exec 文件并将数据合并为一个exec -->
   <target name="merge">
       <jacoco:merge destfile="./jacoco/merge.exec">
            <fileset dir="./jacoco/all" includes="*.exec" />
       </jacoco:merge>
   </target>

    <!-- 3 生成覆盖率报告 -->
   <target name="report">
        <jacoco:report>
            
          <executiondata>
              <file file="${JacocoExec}" />
          </executiondata>
          
          <structure name="JaCoCo Report">          
              <group name="Core">
                  <classfiles>
                      <fileset dir="${JacocoClassPath}/lib/core" />
                  </classfiles>
                  <sourcefiles encoding="${Encoding}">
                        <fileset dir="${JacocoSrcPath}/src/monitor/timeout"/>
                        <fileset dir="${JacocoSrcPath}/src/tools/utils"/>
                        <fileset dir="${JacocoSrcPath}/src/redis/link"/>
                        <fileset dir="${JacocoSrcPath}/src/redis/util"/>
                        <fileset dir="${JacocoSrcPath}/src/server/init"/>
                        <fileset dir="${JacocoSrcPath}/src/grgbpm/core"/>
                        <fileset dir="${JacocoSrcPath}/src/grgbpm/handler"/>
                        <fileset dir="${JacocoSrcPath}/src/server/core"/>
                        <fileset dir="${JacocoSrcPath}/src/server/backend"/>
                        <fileset dir="${JacocoSrcPath}/src/server/exception"/>
                        <fileset dir="${JacocoSrcPath}/src/server/audit"/>
                        <fileset dir="${JacocoSrcPath}/src/server/dao"/>
                        <fileset dir="${JacocoSrcPath}/src/server/log"/>
                        <fileset dir="${JacocoSrcPath}/src/server/reload"/>
                        <fileset dir="${JacocoSrcPath}/src/server/business"/>                       
                        <fileset dir="${JacocoSrcPath}/src/component/service/http"/>
                        <fileset dir="${JacocoSrcPath}/src/component/service/https"/>
                        <fileset dir="${JacocoSrcPath}/src/component/service/webservice"/>
                        <fileset dir="${JacocoSrcPath}/src/component/unpack/separativesign"/>
                        <fileset dir="${JacocoSrcPath}/src/component/pack/separativesign"/>
                        <fileset dir="${JacocoSrcPath}/src/component/unpack/struct"/>
                        <fileset dir="${JacocoSrcPath}/src/component/pack/iso8583"/>
                        <fileset dir="${JacocoSrcPath}/src/component/pack/struct"/>
                        <fileset dir="${JacocoSrcPath}/src/component/unpack/xml"/>
                        <fileset dir="${JacocoSrcPath}/src/component/pack/xml"/>
                        <fileset dir="${JacocoSrcPath}/src/component/unpack/iso8583"/>
                        <fileset dir="${JacocoSrcPath}/src/component/service/tcp"/>
                        <fileset dir="${JacocoSrcPath}/src/component/communicate/ftp"/>
                        <fileset dir="${JacocoSrcPath}/src/component/communicate/http"/>
                        <fileset dir="${JacocoSrcPath}/src/component/communicate/https"/>
                        <fileset dir="${JacocoSrcPath}/src/component/communicate/webservice"/>
                        <fileset dir="${JacocoSrcPath}/src/component/communicate/tcp"/>
                        <fileset dir="${JacocoSrcPath}/src/component/timeout"/>
                        <fileset dir="${JacocoSrcPath}/src/component/endflow"/>
                        <fileset dir="${JacocoSrcPath}/src/component/logic"/>
                        <fileset dir="${JacocoSrcPath}/src/component/encryptor"/>
                        <fileset dir="${JacocoSrcPath}/src/component/judge"/>
                        <fileset dir="${JacocoSrcPath}/src/component/option"/>
                        <fileset dir="${JacocoSrcPath}/src/component/startflow"/>
                        <fileset dir="${JacocoSrcPath}/src/component/format"/>
                  </sourcefiles>
              </group>
              
              <group name="Project">           
                  <classfiles>
                      <fileset dir="${JacocoClassPath}/project"/>
                  </classfiles>
                  <sourcefiles encoding="${Encoding}">
                      <fileset dir="${JacocoSrcPath}/src/project">
                            <exclude name="config/**" />
                      </fileset>
                  </sourcefiles>
              </group>
          </structure>
          
          <html destfile="${JacocoReport}" encoding="${Encoding}" footer="${ReportFooter}"/>
          
      </jacoco:report>
   </target>   
  
</project>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • ANT build.xml文件详解(一) Ant的概念 可能有些读者并不连接什么是Ant以及入可使用它,但只要使用...
    SkTj阅读 3,780评论 0 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,358评论 6 343
  • mean to add the formatted="false" attribute?.[ 46% 47325/...
    ProZoom阅读 2,615评论 0 3
  • “两姓联姻,一堂缔约,良缘永结,匹配同称。看此日桃花灼灼,宜室宜家,卜他年瓜瓞绵绵,尔昌尔炽。谨以白头之约,书向鸿...
    夕噗阅读 353评论 0 0