简介
什么是 Jacoco
Jacoco 是一个开源的代码覆盖率工具,可以嵌入到 Ant 、Maven 中,并提供了 EclEmma Eclipse 插件,也可以使用 Java Agent 技术监控 Java 程序。很多第三方的工具提供了对 Jacoco 的集成,如 sonar、Jenkins 等。
什么是代码覆盖率
代码覆盖(Code coverage)是软件测试中的一种度量,描述程序中源代码被测试的比例和程度,所得比例称为代码覆盖率。
测试覆盖率是对测试完全程度的评测。一方面可以衡量测试工作本身的有效性,提升测试效率,另一方面可以提升代码质量,减少 bug,提升产品的可靠性,稳定性。
代码覆盖分为下面几种情况:
函数覆盖
函数覆盖(Function Coverage),执行到程序中的每一个函数(方法)。
语句覆盖
语句覆盖(Statement Coverage),又称行覆盖(Line Coverage),段覆盖(Segment Coverage),基本块覆盖(Basic Block Coverage),这是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了。这里说的是“可执行语句”,因此就不会包括像 Java 的 import、代码注释、空行等等。只统计能够执行的代码被执行了多少行。需要注意的是,单独一行的花括号{}
也常常被统计进去。语句覆盖常常被人指责为“最弱的覆盖”,它只管覆盖代码中的执行语句,却不考虑各种分支的组合等等。测试效果的不明显,很难更多地发现代码中的问题。
判断覆盖
判断覆盖(Decision Coverage),又称分支覆盖(Branch Coverage),所有边界覆盖(All-Edges Coverage),基本路径覆盖(Basic Path Coverage),判定路径覆盖(Decision-Decision-Path)。它度量程序中每一个判定的分支是否都被测试到了。即代码中每个判断的取真分支和取假分支是否各被覆盖至少各一次。
比如,对于 if(a > 0 && b > 0)
,就要求覆盖“a>0 && b>0”为 true
和 false
各一次。
条件覆盖
条件覆盖(Condition Coverage)指判定中的每个条件的可能取值至少满足一次,它度量判定中的每个子表达式结果true
和false
是否被测试到了。
比如,对于 if(a > 0 && b > 0)
,就要求“a>0”取 true
和 false
各一次,同时要求“b>0”取 true
和 false
各一次。
Jacoco 的功能
覆盖率计数器
Jacoco 使用一系列的不同的计数器来做覆盖率的度量计算。所有这些计数器都是从 Java 的 class 文件中获取信息,这些 class 文件可以(可选)包含调试的信息在里面。即使在没有源码的情况下,这种方法也可以实时有效地对应用程序进行度量和分析。在大部分情况下,收集到的信息可以映射到源码,可视化到每一行代码的粒度。但这种方法还是有一些限制。这些 class 文件必须使用调试信息来编译,这样才可以计算行的覆盖率和提供出源码的高亮。但不是所有的 Java 语言的结构都可以直接编译成一致的二进制代码。在这种情况下,Java 编译器会创建所谓的“合成”代码,会导致产生一些不期望得到的覆盖率结果。
指令覆盖率
Jacoco 最小的计数单元是单个 Java 二进制代码指令。指令覆盖率提供了代码是否被执行的信息。这个度量完全独立源码格式,并且总是可用,即使 class 文件里面没有调试信息。
分支覆盖率
Jacoco 也计算分支的覆盖率,包括所有的if
和switch
语句。这个度量计算一个方法里面的总分支数,确定执行和不执行的分支数量。分支覆盖率总是可用的,即使 class 文件里面没有调试信息。
注意:异常处理是不在分支度量里面统计的。
Jacoco 的应用
Jacoco 提供以下几种开箱即用的工具
- Ant Tasks:配合 Ant 任务使用,可以用于构建,也可以用于远程、本地、离线分析
- Maven Plug-in:配合 Maven 插件使用,一般用于构建
- Java Agent:Java 代理,与 JVM 一起运行,用于收集运行数据供分享使用
- Command Line Interface:命令行,一般用户离线分析
功能测试
基于 Ant Tasks 和 Java Agent,在需要收集的 Java 应用上绑定 Java Agent,可以将运行的 dump 数据输出到本地硬盘供分析工具使用,也可以通过网络将 dump 数据发送到分析服务或开启 tcp 端口供分析工具采集,下面我们采用最后一种方式,开启远程 JVM 的 tcp 端口,在本地通过 Ant Tasks 获取 dump 数据,生成报告。具体步骤如下。
安装 Ant
下载 Apache Ant
https://ant.apache.org/bindownload.cgi
安装 Apache Ant
下载完 Apache Ant 的压缩包后,解压,放到常用的软件安装路径下即可。Apache Ant 是基于 Java的,所以要先确保系统中已经安装了 JDK/JRE。然后将其 bin
目录添加到环境变量 Path
中。执行如下命令检查是否安装成功:
ant -version
Apache Ant(TM) version 1.10.11 compiled on July 10 2021
能正常输出版本号,说明 Apache Ant 安装配置成功。
重新打包待检测的代码
为保证结果的准确性,代码需要使用 debug 模式编译。
Maven 示例:
./mvnw clean package -Dmaven.compiler.debug=true -Dmaven.compiler.debuglevel=lines,vars,source
- -Dmaven.compiler.debug=true:使用 debug 默认编译
- -Dmaven.compiler.debuglevel=lines,vars,source:debug 模式级别
安装 Java Agent
JDK 在 1.5 以后,提供了 agent 技术来构建一个独立于应用程序的代理程序(即为 Agent),用来协助监测、运行甚至替换其他 JVM 上的程序。使用它可以实现虚拟机级别的 AOP 功能。
只需要按如下格式添加虚拟机启动参数,即可关联指定的 Agent。
-javaagent:/path/to/jar=p1=v1,p2=v2
Tomcat
打开 tomcat 的 bin 目录下的 catalina.bat 文件,在 JAVA_OPTS
参数中加入 Java Agent 相关的配置。
示例:
JAVA_OPTS=-server -Xms1024m -Xmx1024m -Djava.awt.headless=true -javaagent:~/jacoco/lib/jacocoagent.jar=includes=com.hundsun.*,output=tcpserver,port=8229,address=127.0.0.1 -Xverify:none
参数说明如下:
- -javaagent:的后面跟jacoco的安装路径
- includes:选择要监测的包名,
*
表示当前包以及子包所有类,多个包使用冒号:
分隔 - port:开放 jacoco 的端口,与 tomcat 端口不能一样,与其他端口也不能冲突
- address:开放端口绑定的 IP
- -Xverify:none:避免启动报错的情况
这样配置后就将 jacoco 嵌入到了 tomcat JVM 中,tomcat 启动后,就可以通过开放的端口,来访问 jacoco 获取数据。
Spring Boot Jar
由于 Spring Boot Jar 是直接通过命令启动的,因此直接在启动命令中添加 Java Agent 相关参数即可。
示例:
java -server -Xms1024m -Xmx1024m -Djava.awt.headless=true javaagent:~/jacoco/lib/jacocoagent.jar=includes=com.hundsun.*,output=tcpserver,port=8229,address=127.0.0.1 -Xverify:none -jar app.jar
构建 Ant 脚本并运行
Ant build.xml 示例:
<?xml version="1.0" ?>
<project default="jacoco" name="Ant Task to execute JaCoCo" xmlns:jacoco="antlib:org.jacoco.ant">
<property name="result.dir" location="./target"/>
<property name="jacocoant.path" value="./jacoco/lib/jacocoant.jar"/>
<property name="result.report.dir" location="${result.dir}/site/jacoco"/>
<property name="result.exec.file" location="${result.dir}/site/jacoco/jacoco.exec"/>
<!-- javaagent 服务的 IP 地址 -->
<property name="server.ip" value="127.0.0.1"/>
<!-- javaagent 端口 -->
<property name="server.port" value="8229"/>
<!--合并报告路径-->
<property name="result.merge.dir" value="./target/jacoco/report"/>
<!-- Step 1: Import JaCoCo Ant tasks -->
<taskdef resource="org/jacoco/ant/antlib.xml" uri="antlib:org.jacoco.ant">
<classpath path="${jacocoant.path}"/>
</taskdef>
<!-- Step 2: dump -->
<target name="dump">
<jacoco:dump address="${server.ip}" append="true" destfile="${result.exec.file}" port="${server.port}" reset="false"/>
</target>
<target name="merge">
<jacoco:merge destfile="${result.report.dir}/merged.exec">
<fileset dir="${result.merge.dir}" includes="*.exec"/>
</jacoco:merge>
</target>
<target name="report" depends="dump">
<!-- 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="JaCoCo Ant Example">
<classfiles>
<fileset dir="./target/classes"/>
</classfiles>
<sourcefiles encoding="UTF-8">
<fileset dir="./src/main/java"/>
</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>
</project>
执行命令 ant report
,生成代码覆盖报告。
单元测试与集成测试
基于 Maven Plug-in 检测单元测试和集成测试的代码覆盖率。
Maven pom.xml
<properties>
<jacoco-maven-plugin.version>0.8.5</jacoco-maven-plugin.version>
<jacoco.utReportFolder>${project.build.directory}/jacoco/test</jacoco.utReportFolder>
<jacoco.utReportFile>${jacoco.utReportFolder}/test.exec</jacoco.utReportFile>
<jacoco.itReportFolder>${project.build.directory}/jacoco/integrationTest</jacoco.itReportFolder>
<jacoco.itReportFile>${jacoco.itReportFolder}/integrationTest.exec</jacoco.itReportFile>
<junit.utReportFolder>${project.testresult.directory}/test</junit.utReportFolder>
<junit.itReportFolder>${project.testresult.directory}/integrationTest</junit.itReportFolder>
</properties>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin.version}</version>
<executions>
<execution>
<id>pre-unit-tests</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<!-- Sets the path to the file which contains the execution data. -->
<destFile>${jacoco.utReportFile}</destFile>
</configuration>
</execution>
<!-- Ensures that the code coverage report for unit tests is created after unit tests have been run -->
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>${jacoco.utReportFile}</dataFile>
<outputDirectory>${jacoco.reportFolder}</outputDirectory>
</configuration>
</execution>
<execution>
<id>pre-integration-tests</id>
<goals>
<goal>prepare-agent-integration</goal>
</goals>
<configuration>
<!-- Sets the path to the file which contains the execution data. -->
<destFile>${jacoco.itReportFile}</destFile>
</configuration>
</execution>
<!-- Ensures that the code coverage report for integration tests is created after integration tests have been run -->
<execution>
<id>post-integration-tests</id>
<phase>post-integration-test</phase>
<goals>
<goal>report-integration</goal>
</goals>
<configuration>
<dataFile>${jacoco.itReportFile}</dataFile>
<outputDirectory>${jacoco.reportFolder}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
执行 ./mvnw clean verify
将完成单元测试和集成测试,测试覆盖率报告也将产生。
注意:所有使用 CCM Adapt 项目已集成
jacoco-maven-plugin
。
FAQ
测试环境经常重启,如何保证一段时间的数据不丢失?
Jacoco 在导出数据文件 .exec
时,默认是 append
模式,也就是当数据文件存在时,新导出的数据被追加到已存在的文件中,对于由 JVM 直接输出到磁盘文件的方法启动的,不需要特殊处理;对于使用 Ant Task 远程获取数据的,可将 Ant Task 设置为定时任务,或使用流水线调用 Ant Task 在重新部署前导出数据。
在执行 ant report
生成报告时,控制台出现 Execution data for class **** does not match
格式的提示,HTML 报告中无内容,或内容不全是怎么回事?
出现这个问题是由于生成报告的 Class (一般是本地项目编译)和运行环境 Class (一般由流水线构建)的 class ids
不一致,导致无法关联,原因可能是:
- 不同的编译器厂商(例如: Eclipse vs. Oracle JDK)
- 不同的编译器版本
- 不同的编译设置选项(debug vs. non-debug)
针对第二条,不同的大版本的 class 肯定是存在兼容性问题的,笔者测试了相同大版本,不同的小版本的 class 是可以关联的,但也可能存在某些小版本有不兼容的,建议使用最新。
如何实现增量代码的覆盖率统计
jacoco 本身不支持输出增量测试覆盖率报告,可以借助脚本工具 diff-cover 与 jacoco 配合使用实现。