Jacoco覆盖率使用总结

2018-07-09  本文已影响406人  LensAclrtn

tags: Java

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

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" 

其中常用选项的含义如下

修改好以后启动 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. 注意事项

导入失败

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>
上一篇 下一篇

猜你喜欢

热点阅读