一、简介

JaCoCo为基于Java VM环境中的代码覆盖率分析提供了一种新的标准技术;提供了一个轻量级、灵活和文档完整的库以便与各种构建(Ant任务)和开发工具集成(Maven插件等)。

功能特性:

  • 覆盖范围包括:指令(C0)、分支(C1)、行、方法、类型和循环复杂度

  • 基于Java字节码文件,因此也可以在没有源码的情况下分析

  • 通过远程协议和JMX控件,可以在任何时候请求代理(agent)提供的执行数据(data dumps)

  • 支持多种覆盖率报告格式:HTML、 XML、 CSV

  • Ant收集和管理执行数据,创建结构化的覆盖报告

非功能特性:

  • 轻量级实现,尽量减少对外部库和系统资源的依赖

  • 良好的性能和最小的运行时开销,特别是对于大型项目

  • 可以和构建脚本、工具等集成

  • 丰富的文档和API

二、安装

https://www.jacoco.org/jacoco/选择版本下载后解压即可。

除此之外,还需安装Java(1.5以上)和Ant(1.7.0以上)。
  • 样例

在解压后的doc\examples\build目录中,使用Ant运行build.xml

构建成功后,在当前目录下会生成target目录,打开target\site\jacoco\index.html即可看到生成的覆盖率报告:

三、Ant集成

Jacoco Ant任务都定义在lib/jacocoant.jar中,可以使用通常的taskdef声明包含在Ant构建xml配置中:

build.xml

<project name="Example" xmlns:jacoco="antlib:org.jacoco.ant">

	<!-- Step 1: Import JaCoCo Ant tasks -->
    <taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
        <classpath path="path_to_jacoco/lib/jacocoant.jar"/>
    </taskdef>

    ...

</project>

常用任务如下:

1、coverage任务

启动Java程序的标准Ant任务是Java、junit和testng,为了给这些任务添加代码覆盖率统计,可以简单地用coverage(覆盖率)任务包装:

<jacoco:coverage>
    <java classname="org.jacoco.examples.HelloJaCoCo" fork="true">
        <classpath>
            <pathelement location="./bin"/>
        </classpath>
    </java>
</jacoco:coverage>
<jacoco:coverage>
    <junit fork="true" forkmode="once">
        <test name="org.jacoco.examples.HelloJaCoCoTest"/>
        <classpath>
            <pathelement location="./bin"/>
        </classpath>
    </junit>
</jacoco:coverage>

覆盖率结果信息在执行过程中收集,并在进程终止时写入文件。

注意:嵌套任务总是必须声明fork为true,否则覆盖率任务将不能记录覆盖率信息,并且会失败。

覆盖率coverage任务只能包装一个任务,它可配置的常用属性如下:

  • enabled

是否为所包含的任务收集覆盖范围数据,默认为true。

  • destfile

执行数据(execution data)的输出文件的路径,默认为jacoco.exec

  • append

如果为true,当执行数据文件已经存在时,覆盖率数据被追加到现有文件中;如果为false,则直接替换现有文件;默认为ture。

  • includes

执行分析中应包括的类名列表,列表项用冒号:分隔,并可以使用通配符*?;除性能优化或技术特殊情况外,通常不需要设置此选项。默认值为*

  • excludes

从执行分析中排除的类名称的列表,与includes属性类似;默认值为空。

  • dumponexit

如果设置为true,则覆盖率数据将在VM关闭时写入;默认为true。

  • port

当output属性设置为tcpserver时绑定的端口,或output属性设置为tcpclient时连接到的端口。如果多个JaCoCo代理在同一台计算机上运行,则必须指定不同的端口。

  • address

当output属性设置为tcpserver时绑定到的IP地址或主机名,或output属性设置为tcpclient时连接到的IP地址或主机名。tcpserver模式下,值*表示使代理接受任何本地地址上的连接。

  • output

    写入覆盖率数据的输出方式,可选值如下,默认为file:

    • file

      在VM终止时,执行数据写入destfile属性配置的文件中。

    • tcpserver

      代理监听address和port属性指定的地址上的进入连接,执行数据将写入该TCP连接。

    • tcpclient

      启动时代理连接到address和port属性指定的地址,执行数据将写入该TCP连接。

    • none

      不做任何输出。

2、agent任务

如果覆盖率coverage任务不适合启动目标(launch target),还可以使用代理任务来创建Java代理参数;下面定义了一个名为agentvmparam的Ant属性,此属性可以直接被用作Java VM参数:

<jacoco:agent property="agentvmparam"/>

此任务具有与coverage任务相同的属性,外加一个用于指定目标属性名称的附加属性:

  • enabled

    当此属性设置为false时,property的值将设置为空字符串;默认为true。

  • property

    要设置的Ant属性的名称。

3、dump任务

此任务允许从另一个JVM上远程收集执行数据,而不需要停止它;但远程(目标)JVM需要配置一个带有tcpserver输出模式的JaCoCo代理。

<jacoco:dump address="server.example.com" reset="true" destfile="remote.exec"/>

常用属性如下:

  • address

    目标IP地址或DNS名称,默认为localhost。

  • port

    目标TCP端口,默认为6300。

  • retryCount

    尝试与目标建立连接的重试次数;此属性可用于等待,直到目标JVM启动成功;默认为10。

  • dump

    标记是否应该转储执行数据;默认为true。

  • reset

    标记转储之后的目标代理中是否应该重置执行数据;默认为false。

  • destfile

    收集的执行数据文件写入位置。

  • append

    如果为true,当执行数据文件已经存在时,覆盖率数据被追加到现有文件中;如果为false,则直接替换现有文件;默认为ture。

4、merge任务

此任务可以将多个测试运行的执行数据合并到单个数据中存储:

<jacoco:merge destfile="merged.exec">
    <fileset dir="executionData" includes="*.exec"/>
</jacoco:merge>

其中destfile属性(合并的执行数据写入的文件位置)必须设置。

5、report任务

此任务可以用来创建不同的报告;一个report任务由不同的部分组成:其中两个部分指定输入数据,其他部分指定输出格式:

<jacoco:report>

    <executiondata>
        <file file="jacoco.exec"/>
    </executiondata>

    <structure name="Example Project">
        <classfiles>
            <fileset dir="classes"/>
        </classfiles>
        <sourcefiles encoding="UTF-8">
            <fileset dir="src"/>
        </sourcefiles>
    </structure>

    <html destdir="report"/>

</jacoco:report>

子元素介绍如下:

  • executiondata

此元素中可以指定Ant资源和JaCoCo执行数据文件,如果指定了多个执行数据文件,则合并执行数据。

  • structure

    此元素定义了报告结构,它有以下可选属性:

    • encoding

      源文件的字符编码,默认为平台默认编码。

    • tabwidth

      制表符的空格字符数,默认为4个字符。

    此元素可能包含以下嵌套元素:

    • classfiles

      包含Ant资源和资源集合的容器元素,可以指定Java class文件、归档文件(jar、war、ear等)或包含class文件的文件夹。归档文件和目录将会被递归搜索其中的class文件。

    • sourcefiles

      此元素可选。它也是包含Ant资源和资源集合的容器元素,用于指定相应的源(source)文件。如果指定了源文件,则报告中会按特定格式突出显示源代码。源文件可以指定为单个文件或目录。

    classfiles和sourcefiles元素接受任何Ant资源集合;因此也可以过滤class文件来缩小报告的范围:

      <classfiles>
          <fileset dir="classes">
              <include name="org/jacoco/examples/important/**/*.class"/>
          </fileset>
      </classfiles>
    

    structure元素还可以通过group元素来按层次结构进行细化,这样覆盖率报告可以反映出软件项目的不同模块的情况;对于每个group元素,可以分别指定相应的class文件和源文件:

      <structure name="Example Project">
          <group name="Server">
              <classfiles>
                  <fileset dir="${workspace.dir}/org.jacoco.example.server/classes"/>
              </classfiles>
              <sourcefiles>
                  <fileset dir="${workspace.dir}/org.jacoco.example.server/src"/>
              </sourcefiles>
          </group>
          <group name="Client">
              <classfiles>
                  <fileset dir="${workspace.dir}/org.jacoco.example.client/classes"/>
              </classfiles>
              <sourcefiles>
                  <fileset dir="${workspace.dir}/org.jacoco.example.client/src"/>
              </sourcefiles>
          </group>
    
          ...
    
      </structure>
    

    structure元素和group元素必须设置name属性(表示结构或组的名称)。

  • html

    创建HTML格式的多页(multi-page)报告,可以将报告以多个文件的形式写入一个目录或压缩到单个ZIP文件中。常用属性如下,其中destdir属性或destfile属性必须设置一个:

    • destdir

      在指定的目录中创建覆盖率报告。

    • destfile

      在Zip文件中创建覆盖率报告。

    • footer

      每个报告页面的页脚文本,默认没有页脚。

    • encoding

      生成的HTML页面的字符编码,默认为UTF-8。

  • xml

    创建XML格式的单文件报表,属性如下:

    • destfile

      生成报告文件的位置。

    • encoding

      生成的XML文档的编码,默认为UTF-8。

  • csv

    以CSV文件格式创建单文件报告,属性同xml元素。

  • check

    此元素不会创建覆盖率报告,它会检查覆盖计数器并报告违反配置规则的情况。每个规则都应用于给定类型的元素(类、包等),并且有一个限制列表,对每个元素进行检查。例如:下面的样例表示检查每个包的行覆盖率至少是80% ,没有漏掉任何class的情况:

      <check>
          <rule element="PACKAGE">
              <limit counter="LINE" value="COVEREDRATIO" minimum="80%"/>
              <limit counter="CLASS" value="MISSEDCOUNT" maximum="0"/>
          </rule>
      </check>
    

    详细属性配置参考官方文档

四、样例

1、简单样例

  • 源文件结构

MyApp.java

package com.test;

public class MyApp {

	public static void main(String[] args) {
		System.out.print("[" + TimeService.getTime() + "]: ");
		sayHi(System.currentTimeMillis());
	}
	
	private static void sayHi(long num){
		if(num % 2 == 0){
			System.out.println("Today is a good day, I'm so happy.");
		}else{
			System.out.println("I'm in a bad mood.");
		}
	}
}

TimeService.java

package com.test;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TimeService {

	public static String getTime(){
		return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
	}
}
  • build.xml
<?xml version="1.0" encoding="UTF-8"?>

<project default="build" xmlns:jacoco="antlib:org.jacoco.ant">

	<property name="src.dir" location="./src" />
	<property name="result.dir" location="./target" />
	<property name="result.classes.dir" location="${result.dir}/classes" />
	<property name="result.report.dir" location="${result.dir}/report" />
	<property name="result.exec.file" location="${result.dir}/jacoco.exec" />

	<!-- Step 1: Import JaCoCo Ant tasks -->
	<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
		<classpath path="../JaCoCo/lib/jacocoant.jar" />
	</taskdef>

	<!-- 清空target目录 -->
	<target name="clean">
		<delete dir="${result.dir}" />
	</target>

	<!-- 编译 -->
	<target name="compile">
		<mkdir dir="${result.classes.dir}" />
		<javac srcdir="${src.dir}" destdir="${result.classes.dir}" debug="true" includeantruntime="false" />
	</target>

	<target name="run" depends="compile">
		<!-- Step 2: Wrap test execution with the JaCoCo coverage task -->
		<jacoco:coverage destfile="${result.exec.file}">
			<java classname="com.test.MyApp" fork="true">
				<classpath path="${result.classes.dir}" />
			</java>
		</jacoco:coverage>
	</target>

	<target name="report" depends="run">
		<!-- Step 3: Create coverage report -->
		<jacoco:report>

			<!-- This task needs the collected execution data and ... -->
			<executiondata>
				<file file="${result.exec.file}" />
			</executiondata>

			<!-- the class files and optional source files ... -->
			<structure name="My App">
				<classfiles>
					<fileset dir="${result.classes.dir}" />
				</classfiles>
				<sourcefiles encoding="UTF-8">
					<fileset dir="${src.dir}" />
				</sourcefiles>
			</structure>

			<!-- to produce reports in different formats. -->
			<html destdir="${result.report.dir}" />
			<csv destfile="${result.report.dir}/report.csv" />
			<xml destfile="${result.report.dir}/report.xml" />
		</jacoco:report>
	</target>

	<target name="build" depends="clean,compile,run,report" />

</project>

使用ant构建后,可以打开target\report\index.html查看生成的HTML类型的覆盖率报告:

2、复杂样例

使用代理、分组等实现覆盖率报告。

测试程序向服务器发送请求,服务器使用代理启动,代理参数如下:

-javaagent:F:/NewDivide/Java/JaCoCo/jacoco/lib/jacocoagent.jar=output=tcpserver,port=6060,address=127.0.0.1
如果使用Eclipse,需要在Configure中按如上配置JVM参数。
  • 服务器

    服务器提供Web服务,当请求http://localhost:8080/myweb/web?a=1&b=2时执行相关逻辑计算a+b的值,并将结果返回。

    • Serlvet:
      package com.web;
    
      import java.io.IOException;
      import java.io.PrintWriter;
    
      import javax.servlet.ServletException;
      import javax.servlet.http.HttpServlet;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
    
      import com.app.CalcUtil;
      import com.test.MyApp;
      import com.test.TimeService;
    
      public class MyWeb extends HttpServlet {
    
          public void doGet(HttpServletRequest request, HttpServletResponse response)
                  throws ServletException, IOException {
    
              PrintWriter out = response.getWriter();
              System.out.print("[" + TimeService.getTime() + "]: ");
              MyApp.sayHi(System.currentTimeMillis());
              int a = Integer.parseInt(request.getParameter("a"));
              int b = Integer.parseInt(request.getParameter("b"));
              out.print(CalcUtil.add(a, b));
              out.flush();
              out.close();
          }
    
          public void doPost(HttpServletRequest request, HttpServletResponse response)
                  throws ServletException, IOException {
              doGet(request, response);
          }
      }
    
    

    服务器还依赖两个jar(工程)包:my-project.jaranother-project.jar

    • my-project.jar

        src
        └─com
            └─test
                    MyApp.java
                    TimeService.java
      
      • MyApp.java
        package com.test;
      
        public class MyApp {
      
            public static void sayHi(long num){
                if(num % 2 == 0){
                    System.out.println("Today is a good day, I'm so happy.");
                }else{
                    System.out.println("I'm in a bad mood.");
                }
            }
        }
      
      • TimeService.java
        package com.test;
      
        import java.text.SimpleDateFormat;
        import java.util.Date;
      
        public class TimeService {
      
            public static String getTime(){
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
            }
        }
      
    • another-project.jar

        ─src
         └─com
             └─app
                     CalcUtil.java
      
      • CalcUtil.java
        package com.app;
      
        public class CalcUtil {
      
            public static int add(int a, int b){
                return a + b;
            }
        }
      
  • 测试程序

测试程序依赖httpclient-4.5.6.jarhttpcore-4.4.10.jarcommons-logging-1.1.2.jar,源码如下:

package com.test;

import java.nio.charset.StandardCharsets;

import org.apache.http.HttpStatus;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class MyTest {

	public static void main(String[] args) {
        try {
        	CloseableHttpClient httpClient = HttpClients.createDefault();
        	HttpGet request = new HttpGet("http://localhost:8080/myweb/web?a=1&b=2");
			CloseableHttpResponse response = httpClient.execute(request);
			if(HttpStatus.SC_OK == response.getStatusLine().getStatusCode()){
				String result = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
				System.out.println("The result is: " + result);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} 
	}
}
  • 目录说明

目录结构如下,source目录存放源码,libs目录存放jar,包括测试程序和需要统计覆盖率的服务器程序的jar:

执行Ant构建时,会在当前目录下生成target目录;覆盖率会生成在target/report目录下。

  • build.xml
<?xml version="1.0" encoding="UTF-8"?>

<project default="report" xmlns:jacoco="antlib:org.jacoco.ant">

	<property name="app.lib" location="./libs/app" />
	<property name="classes.lib" location="./libs/classes" />
	<property name="src.dir" location="./source" />
	<property name="result.dir" location="./target" />
	<property name="result.report.dir" location="${result.dir}/report" />
	<property name="result.exec.file" location="${result.dir}/jacoco.exec" />

	<taskdef uri="antlib:org.jacoco.ant" resource="org/jacoco/ant/antlib.xml">
		<classpath path="../JaCoCo/lib/jacocoant.jar" />
	</taskdef>

	<target name="clean">
		<delete dir="${result.dir}" />
	</target>

	<target name="run" depends="clean">
		<jacoco:coverage destfile="${result.exec.file}">
			<java classname="com.test.MyTest" fork="true">
				<classpath path="${app.lib}/*" />
			</java>
		</jacoco:coverage>
	</target>

	<target name="dump" depends="run">
        <jacoco:dump address="127.0.0.1" reset="true" destfile="${result.exec.file}" port="6060" />
    </target>

	<target name="report" depends="dump">
		<jacoco:report>
			<executiondata>
				<file file="${result.exec.file}" />
			</executiondata>

			<structure name="My App">
				<group name="my-project">
					<classfiles>
						<fileset dir="${classes.lib}">
							<include name="my-project.jar" />
						</fileset>
					</classfiles>
					<sourcefiles encoding="UTF-8">
						<fileset dir="${src.dir}/my-project/src" />
					</sourcefiles>
				</group>
				<group name="another-project">
					<classfiles>
						<fileset dir="${classes.lib}">
							<include name="another-project.jar" />
						</fileset>
					</classfiles>
					<sourcefiles encoding="UTF-8">
						<fileset dir="${src.dir}/another-project/src" />
					</sourcefiles>
				</group>
			</structure>

			<html destdir="${result.report.dir}" />
		</jacoco:report>
	</target>

</project>
  • 构建

  • 覆盖率

参考资料:

EclEmma - JaCoCo Java Code Coverage Library

JaCoCo - Java Code Coverage Library

JaCoCo - Documentation

JaCoCo - Java Agent

JaCoCo - Ant Tasks

JaCoCo - Command Line Interface