JaCoCo简介
一、简介
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.jar
和another-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.jar
、httpcore-4.4.10.jar
和commons-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