TestSuperTestman_博客已迁移效率与工具

Jacoco Code Coverage

2017-05-13  本文已影响6896人  纳爱斯

Java Jacoco Ant Maven

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

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

具体见下表:

工具 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 收集集成测试代码覆盖率

什么是集成测试?
启动 Tomcat 服务之后,ps 一下,如果在 Tomcat 服务中有 jacocoagent 这个服务的话
那么恭喜你,你成功了!!!
<?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 收集单元测试代码覆盖率

           <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 工程下载

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

jacoco.xml report report report
Tip:
绿色部分:完全覆盖
黄色部分:条件覆盖
红色部分:未覆盖
<?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

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

猜你喜欢

热点阅读