基于jacoco生成测试环境代码覆盖率报告

Jacoco 统计代码覆盖率

简介

什么是 Jacoco

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

什么是代码覆盖率

代码覆盖(Code coverage)是软件测试中的一种度量,描述程式中源代码被测试的比例和程度,所得比例称为代码覆盖率。

代码覆盖率是衡量测试质量的一个重要指标。在对一个软件产品进行了单元测试、组装测试、集成测试以及接口测试等繁多的测试之后,我们能不能就此对软件的质量产生一定的信心呢?这就需要我们对测试的质量进行考察。如果测试仅覆盖了代码的一小部分,那么不管我们写了多少测试用例,我们也不能相信软件质量是有保证的。相反,如果测试覆盖到了软件的绝大部分代码,我们就能对软件的质量有一个合理的信心。

代码覆盖分为下面五种情况:

函数覆盖

函数覆盖(Function Coverage),执行到程序中的每一个函数(或副程式)。

语句覆盖

语句覆盖(Statement Coverage),又称行覆盖(Line Coverage),段覆盖(Segment Coverage),基本块覆盖(Basic Block Coverage),这是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了。这里说的是“可执行语句”,因此就不会包括像C++的头文件声明、代码注释、空行等等。非常好理解,只统计能够执行的代码被执行了多少行。需要注意的是,单独一行的花括号{}也常常被统计进去。语句覆盖常常被人指责为“最弱的覆盖”,它只管覆盖代码中的执行语句,却不考虑各种分支的组合等等。测试效果的不明显,很难更多地发现代码中的问题。

判断覆盖

判断覆盖(Decision Coverage),又称分支覆盖(Branch Coverage),所有边界覆盖(All-Edges Coverage),基本路径覆盖(Basic Path Coverage),判定路径覆盖(Decision-Decision-Path)。它度量程序中每一个判定的分支是否都被测试到了。

条件覆盖

条件覆盖(Condition Coverage),它度量判定中的每个子表达式结果truefalse是否被测试到了。

路径覆盖

路径覆盖(Path Coverage),又称断言覆盖(Predicate Coverage)。它度量了是否函数的每一个分支都被执行了。 这句话也非常好理解,就是所有可能的分支都执行一遍,有多个分支嵌套时,需要对多个分支进行排列组合,可想而知,测试路径随着分支的数量指数级别增加。

Jacoco 的功能

覆盖率计数器

Jacoco 使用一系列的不同的计数器来做覆盖率的度量计算。所有这些计数器都是从 Java 的 class 文件中获取信息,这些 class 文件可以(可选)包含调试的信息在里面。即使在没有源码的情况下,这种方法也可以实时有效地对应用程序进行度量和分析。在大部分情况下,收集到的信息可以映射到源码,可视化到每一行代码的粒度。但这种方法还是有一些限制。这些 class 文件必须使用调试信息来编译,这样才可以计算行的覆盖率和提供出源码的高亮。但不是所有的 Java 语言的结构都可以直接编译成一致的二进制代码。在这种情况下,Java 编译器会创建所谓的“合成”代码,会导致产生一些不期望得到的覆盖率结果。

指令覆盖率

Jacoco 最小的计数单元是单个 Java 二进制代码指令。指令覆盖率提供了代码是否被执行的信息。这个度量完全独立源码格式,并且总是可用,即使 class 文件里面没有调试信息。

分支覆盖率

Jacoco 也计算分支的覆盖率,包括所有的ifswitch语句。这个度量计算一个方法里面的总分支数,确定执行和不执行的分支数量。分支覆盖率总是可用的,即使 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 -->
        <mkdir dir="${result.report.dir}" /> 
        <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 将完成单元测试和集成测试,测试覆盖率报告也将产生。

项目Github地址:https://github.com/jacoco/jacoco
官方网站:https://www.jacoco.org/jacoco/

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 是可以关联的,但也可能存在某些小版本有不兼容的,建议使用最新。

感谢鹏程分享 :+1: