一、Why

  随着敏捷开发的流行,版本快速迭代,开发人员由于时间紧迫,在一定程度上也会造成送测代码质量降低,因此编写单元测试已经成为业界共识,良好的单元测试不仅能提升编码质量,也能在整个测试周期的最开始阶段减少很大一部分的缺陷,但如何来度量保证单元测试的质量呢?相比单纯追求单元测试用例的数量,分析单元测试的代码覆盖率是一种更为可行的方式。JaCoCo(Java Code Coverage)就是一种分析单元测试覆盖率的工具,使用它运行单元测试后,可以给出代码中哪些部分被单元测试测到,哪些部分没有没测到,并且给出整个项目的单元测试覆盖情况百分比,看上去一目了然。

 

二、What

  Jacoco 是一个开源的覆盖率工具。Jacoco 可以嵌入到 Ant 、Maven 中,并提供了 EclEmma Eclipse 插件,也可以使用 Java Agent 技术监控 Java 程序。很多第三方的工具提供了对 Jacoco 的集成,如:Sonar、Jenkins、IDEA。

  Jacoco 包含了多种尺度的覆盖率计数器,包含指令级(Instructions,C0 coverage),分支(Branches,C1 coverage)、圈复杂度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、类(Classes)

    → Instructions:Jacoco 计算的最小单位就是字节码指令。指令覆盖率表明了在所有的指令中,哪些被执行过以及哪些没有被执行。这项指数完全独立于源码格式并且在任何情况下有效,不需要类文件的调试信息。

    → Branches:Jacoco 对所有的 if 和 switch 指令计算了分支覆盖率。这项指标会统计所有的分支数量,并同时支出哪些分支被执行,哪些分支没有被执行。这项指标也在任何情况都有效。异常处理不考虑在分支范围内。

在有调试信息的情况下,分支点可以被映射到源码中的每一行,并且被高亮表示。
        红色钻石:无覆盖,没有分支被执行。
        黄色钻石:部分覆盖,部分分支被执行。
        绿色钻石:全覆盖,所有分支被执行。

    → Cyclomatic ComplexityJacoco 为每个非抽象方法计算圈复杂度,并也会计算每个类、包、组的复杂度。根据 McCabe 1996 的定义,圈复杂度可以理解为覆盖所有的可能情况最少使用的测试用例数。这项参数也在任何情况下有效。

    → Lines该项指数在有调试信息的情况下计算。   

因为每一行代码可能会产生若干条字节码指令,所以我们用三种不同状态表示行覆盖率
        红色背景:无覆盖,该行的所有指令均无执行。
        黄色背景:部分覆盖,该行部分指令被执行。
        绿色背景:全覆盖,该行所有指令被执行。

    → Methods每一个非抽象方法都至少有一条指令。若一个方法至少被执行了一条指令,就认为它被执行过。因为 Jacoco 直接对字节码进行操作,所以有些方法没有在源码显示(比如某些构造方法和由编译器自动生成的方法)也会被计入在内。

    → Classes每个类中只要有一个方法被执行,这个类就被认定为被执行。有些没有在源码声明的方法被执行,也认定该类被执行。

 

三、Where

    如何快速了解代码覆盖率以及jacoco

1.jacoco官网

2.Java 覆盖率 Jacoco 插桩的不同形式总结和踩坑记录

3.浅谈代码覆盖率

4.Jacoco Code Coverage

 

四、How

覆盖率工具工作流程

java agent 测试覆盖 java单元测试覆盖率工具_java agent 测试覆盖

1. 对Java字节码进行插桩,On-The-Fly和Offine两种方式。 
2. 执行测试用例,收集程序执行轨迹信息,将其dump到内存。 
3. 数据处理器结合程序执行轨迹信息和代码结构信息分析生成代码覆盖率报告。 
4. 将代码覆盖率报告图形化展示出来,如html、xml等文件格式。

 

插桩原理

java agent 测试覆盖 java单元测试覆盖率工具_tomcat_02

主流代码覆盖率工具都采用字节码插桩模式,通过钩子的方式来记录代码执行轨迹信息。其中字节码插桩又分为两种模式On-The-Fly和Offine。On-The-Fly模式优点在于无需修改源代码,可以在系统不停机的情况下,实时收集代码覆盖率信息。Offine模式优点在于系统启动不需要额外开启代理,但是只能在系统停机的情况下才能获取代码覆盖率。 基于以上特性,同时由于公司使用JDK8,我们采用Jacoco来获取集成测试代码覆盖率,单元测试使用Cobertura。

On-The-Fly插桩 Java Agent
  • JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序
  • 代理程序在每装载一个class文件前判断是否已经转换修改了该文件,如果没有则需要将探针插入class文件中。
  • 代码覆盖率就可以在JVM执行代码的时候实时获取。
  • 典型代表:Jacoco
On-The-Fly插桩 Class Loader
  • 自定义classloader实现自己的类装载策略,在类加载之前将探针插入class文件中
  • 典型代表:Emma
Offine插桩
  • 在测试之前先对文件进行插桩,生成插过桩的class文件或者jar包,执行插过桩的class文件或者jar包之后,会生成覆盖率信息到文件,最后统一对覆盖率信息进行处理,并生成报告。
  • Offline插桩又分为两种:
  • Replace:修改字节码生成新的class文件
  • Inject:在原有字节码文件上进行修改
  • 典型代表:Cobertura
On-The-Fly和Offine比较
  • On-The-Fly模式更加方便的获取代码覆盖率,无需提前进行字节码插桩,可以实时获取代码覆盖率信息
  • Offline模式适用于以下场景:
  • 运行环境不支持java agent
  • 部署环境不允许设置JVM参数
  • 字节码需要被转换成其他虚拟机字节码,如Android Dalvik VM
  • 动态修改字节码过程中和其他agent冲突
  • 无法自定义用户加载类

五、示例

本文将通过idea maven插件启动的方式、war包ant构建的方式逐一演示jacoco是如何进行代码覆盖率采集的。在此之前运行环境准备工作有:

  • jdk 1.8,且配置好环境变量
  • maven, 且配置好环境变量,且保证能从远程仓库拉取dependency
  • ant,且配置好环境变量
  • tomcat 8
  • jenkins,且保证插件能正常安装,通常能直接访问外网的情况下是能正常安装的
  • idea

A.idea+maven+testng

Step 1:创建maven工程,并在pom.xml中配置maven插件和testng依赖

<dependencies>
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>6.11</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.7.9</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.5.1</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

Step 2:编写示例代码,以及单元测试代码

可以看到编写单元测试用例对示例的Count类的Add和Sub方法进行了调用,而Login类的login方法无任何地方调用

示例项目的目录结构

java agent 测试覆盖 java单元测试覆盖率工具_tomcat_03

示例代码

Count.java

public class Count {
    public int add(int x, int y){
        return x + y;
    }

    public int sub(int x, int y){
        return x - y;
    }
}

login.java

public class Login {
    public int login(String username, String password){
        if("123".equals(username) && "123".equals(password)){
            return 1;
        } else{
            return 0;
        }
    }
}

CountTest.java

import org.testng.Assert;
import org.testng.annotations.Test;

public class CountTest {
    @Test
    public void testAdd(){
        Count count = new Count();
        Assert.assertEquals(count.add(1, 2), 3);
    }

    @Test
    public void testSub(){
        Count count = new Count();
        Assert.assertEquals(count.sub(2, 1), 1);
    }
}

Step 3:编译

1.将idea切换至控制台窗口

java agent 测试覆盖 java单元测试覆盖率工具_java_04

2.输入命令:mvn install,如图构建成功

java agent 测试覆盖 java单元测试覆盖率工具_java_05

3.稍等片刻,进入/target/site/jacoco/jacoco-resources/index.html查看汇总报告

java agent 测试覆盖 java单元测试覆盖率工具_java agent 测试覆盖_06

Step 4:查看代码覆盖率检测报告

打开/target/site/jacoco/jacoco-resources/index.html,我们可以看到覆盖率计数器,包含指令级(Instructions,C0 coverage),分支(Branches,C1 coverage)、圈复杂度(Cyclomatic Complexity)、行(Lines)、方法(Non-abstract Methods)、类(Classes),绿色代表覆盖,红色代表未覆盖

java agent 测试覆盖 java单元测试覆盖率工具_代码覆盖率_07

 

 

 

可查看具体的分支覆盖情况

java agent 测试覆盖 java单元测试覆盖率工具_java agent 测试覆盖_08

 

 

 

 

   B.war包+tomcat+ant

Step 1:创建一个java web工程,并打包成war包

1.编写一个示例servlet

 

 

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/TestServlet")
public class TestServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("123");
        if (1==1){
            System.out.println("1==1");
        }
        response.getWriter().println("123");
    }
}

2.打包成war包(具体如何打包请移步百度)

Step 2:配置tomcat catalina.bat启动参数,并启动web项目,注意%TOMCAT_HOME%值得是tomcat目录,根据你的安装目录替换

1.下载jacoco.zip,jacoco.zip

2.将Step 1中的war包移动至%TOMCAT_HOME%\webapps目录下,届时启动后tomcat将自行解包,另外默认启动端口为8080,若不想被占用,可进入%TOMCAT_HOME%\conf\server.xml中修改

3.打开%TOMCAT_HOME%\bin\catalina.bat,添加set "JAVA_OPTS=%JAVA_OPTS% -javaagent:D:\jacoco\lib\jacocoagent.jar=includes=*,output=tcpserver,port=7777,address=192.168.2.133",如果你不知道加在哪,请搜索set "JAVA_OPTS=%JAVA_OPTS% -Djava.protocol.handler.pkgs=org.apache.catalina.webresources"追加至后面就行

  • 启动参数的含义:
jacocoAgentPath:
includes:是指要收集哪些类(注意不要光写包名,最后要写.*),不写的话默认是*,会收集应用服务上所有的类,包括服务器和其他中间件的类,一般要过滤(当然如果你愿意写*也完全没有问题,如:`includes=com.*` or `includes=*`)
output:有4个值,分别是file、tcpserver、tcpclient、mbean,默认是 file。使用 file 的方式只有在停掉应用服务的时候才能产生覆盖率文件,而使用 tcpserver 的方式可以在不停止应用服务的情况下下载覆盖率文件,后面会介绍如何使用 dump 方法来得到覆盖率文件
address:ip地址,就是tomcat 服务器的机器的IP
port:端口地址

4.启动%TOMCAT_HOME%\bin\startup.bat,并确保窗口不会关闭

5.浏览器打开http://localhost:8080/untitled_war exploded2 archive/TestServlet,可正常访问

java agent 测试覆盖 java单元测试覆盖率工具_代码覆盖率_09

 

 

 

 

Step 3:配置jacoco ant dump文件build_dump.xml

java agent 测试覆盖 java单元测试覆盖率工具_java agent 测试覆盖_10

java agent 测试覆盖 java单元测试覆盖率工具_java_11

<?xml version="1.0" ?>
<project name="coverage" default="dump" xmlns:jacoco="antlib:org.jacoco.ant" >
    <!--Jacoco的安装路径-->
  <property name="jacocoantPath" value="D:\jacoco\lib\jacocoant.jar"/>
  <!--最终生成.exec文件的路径,Jacoco就是根据这个文件生成最终的报告的-->
  <property name="jacocoexecPath" value="D:\jacoco_output\jacoco.exec"/>
    <!--生成覆盖率报告的路径-->
  <property name="reportfolderPath" value="D:\jacoco_output\report\"/>
  <!--远程tomcat服务的ip地址-->
  <property name="server_ip" value="192.168.2.133"/>
  <!--前面配置的远程tomcat服务打开的端口,要跟上面配置的一样-->
  <property name="server_port" value="7777"/>
  <!--源代码路径可以包含多个源代码
  <property name="webSrcpath" value="D://jacoco_output//service//src//main//java//" />-->
  <property name="webSrcpath" value="C:\Users\10147\IdeaProjects\untitled\src\" />
  
  <!--.class文件路径可以包含多个,class文件要填写部署在服务器上的路径,jar包要解压>-->
  <property name="webClasspath" value="D:\tomcat8\apache-tomcat-8.5.54\apache-tomcat-8.5.54\webapps\untitled_war exploded2 archive\WEB-INF\classes" />
 

  <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="Launch related">           
                  <classfiles>
                      <fileset dir="${webClasspath}" />
                  </classfiles>
                  <sourcefiles encoding="gbk">
                      <fileset dir="${webSrcpath}" />
                  </sourcefiles>
              </group>
          </structure>

          <html destdir="${reportfolderPath}" encoding="utf-8" />         
      </jacoco:report>
  </target>
</project>

build_dump.xml

java agent 测试覆盖 java单元测试覆盖率工具_java agent 测试覆盖_10

java agent 测试覆盖 java单元测试覆盖率工具_java_11

<?xml version="1.0" ?>
<project name="coverage" default="report" xmlns:jacoco="antlib:org.jacoco.ant" >
    <!--Jacoco的安装路径-->
  <property name="jacocoantPath" value="D:\jacoco\lib\jacocoant.jar"/>
  <!--最终生成.exec文件的路径,Jacoco就是根据这个文件生成最终的报告的-->
  <property name="jacocoexecPath" value="D:\jacoco_output\jacoco.exec"/>
    <!--生成覆盖率报告的路径-->
  <property name="reportfolderPath" value="D:\jacoco_output\report\"/>
  <!--远程tomcat服务的ip地址-->
  <property name="server_ip" value="192.168.2.133"/>
  <!--前面配置的远程tomcat服务打开的端口,要跟上面配置的一样-->
  <property name="server_port" value="7777"/>
  <!--源代码路径可以包含多个源代码
  <property name="webSrcpath" value="D://jacoco_output//service//src//main//java//" />-->
  <property name="webSrcpath" value="C:\Users\10147\IdeaProjects\untitled\src\" />
  
  <!--.class文件路径可以包含多个,class文件要填写部署在服务器上的路径,jar包要解压>-->
  <property name="webClasspath" value="D:\tomcat8\apache-tomcat-8.5.54\apache-tomcat-8.5.54\webapps\untitled_war exploded2 archive\WEB-INF\classes" />
 

  <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="Launch related">           
                  <classfiles>
                      <fileset dir="${webClasspath}" />
                  </classfiles>
                  <sourcefiles encoding="gbk">
                      <fileset dir="${webSrcpath}" />
                  </sourcefiles>
              </group>
          </structure>

          <html destdir="${reportfolderPath}" encoding="utf-8" />         
      </jacoco:report>
  </target>
</project>

build_report.xml

build_dump.xml和build_report.xml,除了project标签下的default属性不一致,因此属性不会再赘述

  • jacocoantPath:即jacoco.zip解压后的jacocoant.jar目录
  • jacocoexecPath:执行dump操作后生成exec文件的目录
  • reportfolderPath:执行report操作后生成的report目录
  • server_ip:tomcat的ip
  • port:添加至tomcat启动参数时指定的port
  • webSrcpath:未编译源的文件的目录,或者说是包含源文件代码的文件,用于查看报告时,定位代码
  • webClasspath:编译后的classes文件目录

Step 4:配置jacoco ant report文件build_report.xml

Step 5:执行检测

切换至build_dump.xml目录下,依次执行“ant -buildfile  build_dump.xml ”、“ant -buildfile  build_report.xml ”命令

java agent 测试覆盖 java单元测试覆盖率工具_java_14

java agent 测试覆盖 java单元测试覆盖率工具_tomcat_15

Step 6:查看测试报告

进入build_report.xml配置的reportfolderPath属性的目录,D:\jacoco_output\report\,查看测试报告index.html

java agent 测试覆盖 java单元测试覆盖率工具_java_16