一、前言
1、本文简介
本文先行介绍什么是JaCoCo与代码覆盖率,然后基于Maven项目,介绍JaCoCo相关依赖包和插件,并示例如何在Spingboot项目中应用JaCoCo工具。
本文示例基于的环境:IDEA 社区版 2021.3.1、Java1.8、SpringBoot 2.6.0、Junit 5.8.1、Maven 3.8.4
2、官网地址
https://www.eclemma.org/jacoco/trunk/index.html
3、什么是JaCoCo
缩写自Java Code Coverage,是一个免费的 Java 代码覆盖库。
4、关于代码覆盖率
代码覆盖率,是软件测试中的一个重要度量,指的是通过计算测试过程中被执行的源代码占全部源代码的比例,进而间接度量软件质量的方法。
该方法广泛用于评估单元测试的覆盖率,此时测试用例由开发人员编写实施,属于白盒测试。
可以顺便去看邹欣写的这本《构建之法 现代软件工程》,其中第二章有提及相关的一些内容:
关于代码覆盖率的更多知识,可以自行搜索阅读,本文篇幅有限,不展开描述。
推荐补充阅读:《什么是代码覆盖率》
5、注意
虽然本文是介绍用JaCoCo来提高测试覆盖率,以此提高代码质量。但是,无需强求百分百的代码覆盖率。只要实践过,就会发现,项目仅需大一点,测试用例的量就会以几何倍数增长,很难或者说需要花费很大代价才能达到很高的覆盖率。所以,实践过程中,需要进行权衡。
二、JaCoCo的依赖包和插件
Maven仓库中的JaCoCo页面(在这里主要是可以直接获得各个版本的POM引入写法示例以及各个依赖包等的介绍,还有最重要的就是可以看到各个依赖包之间的相互依赖关系):https://central.sonatype.com/namespace/org.jacoco
1、jacoco
官方简介:JaCoCo Standalone Distribution
该依赖包是完整的JaCoCo依赖包,内部包含了所有JaCoCo的依赖包,如果不清楚到底要依赖哪些JaCoCo的包,那么可以直接引入这个包的依赖(当然,这样的的话,你的jar包就会偏大,毕竟很可能引入了实际用不上的内容)。
另外,底下的这些包,实际上就是JaCoCo大大小小的模块。
2、jacoco-maven-plugin
若是Maven构建项目,一般只需要引入这个插件就行了。
插件简介:The JaCoCo Maven Plugin provides the JaCoCo runtime agent to your tests and allows basic report creation
包含:build、core、agent、report
官网介绍地址:https://www.jacoco.org/jacoco/trunk/doc/maven.html
注意点击阅读官网介绍页面最底下的这些可配置的goals的页面:
运行JUnit测试并生成单元测试覆盖率报告的POM文件内容官方示例地址:https://www.jacoco.org/jacoco/trunk/doc/examples/build/pom.xml
运行单元测试和集成测试,并创建两个覆盖率报告的POM文件内容官方示例地址:https://www.jacoco.org/jacoco/trunk/doc/examples/build/pom-it.xml
环境要求:Maven 3.0 或更高版本,以及Java 1.8 或更高版本用于 Maven 运行时,Java 1.5 或更高版本用于测试执行程序。
3、org.jacoco.cli
依赖包简介:JaCoCo Command Line Interface
包含:build、core、report
4、org.jacoco.ant
依赖包简介:JaCoCo Ant Tasks
包含:build、core、report、agent
官网介绍地址:https://www.jacoco.org/jacoco/trunk/doc/ant.html
Ant:类似make/nmake的一种构建工具,主要就是用来构建JAVA程序。本文主要提及的Maven就是在Ant之后登场的一个构建工具,再后面现在还有一个就是Gradle。感兴趣可以顺带去搜索了解一下。
回到这个依赖包,如果你使用的是Ant进行Java项目的构建,那么就会用它。它相当于帮你定义了一个任务,可以用于启动带有执行记录的 Java 程序 以及根据记录的数据创建覆盖率报告。执行数据可以通过任务coverage、agent、dump 和 merge 进行收集和管理。不同格式的报告是使用report任务创建。对于离线检测,任务instrument可用于准备类文件。
由于本文不谈及Ant,所以此处不扩展。
5、org.jacoco.agent
依赖包简介:JaCoCo Agent
包含:build
官网介绍地址:https://www.jacoco.org/jacoco/trunk/doc/agent.html
JaCoCo是针对编译后生成的Class文件进行执行并记录覆盖率数据。agent就是负责进行这个即时检测的操作。(检测的最小单位是 Java 字节码指令)
JaCoCo agent 将收集执行信息,并且根据请求或者当JVM 退出时转储收集的数据信息。执行数据输出有三种不同的模式:
①文件系统:在 JVM 终止时,执行数据将写入本地 文件。
②TCP 套接字服务器:外部工具可以连接到 JVM 并通过Socket连接获得执行数据。执行数据重置和执行数据转储在VM 退出时是可能发生的。
③TCP 套接字客户端:在启动时,JaCoCo agent连接到给定的 TCP 端点,执行数据会通过通过Socket连接发送过去。VM 退出时,执行数据重置和执行数据转储是可能发生的。
6、org.jacoco.report
依赖包简介:JaCoCo Report
包含:build、core
负责生成报告文件。
7、org.jacoco.core
依赖包简介:JaCoCo core APIs and implementations
包含:build
8、org.jacoco.build
依赖包介绍:JaCoCo - Java Code Coverage Library
注:作为一个JaCoCo其他依赖包公共引入的Parent,里面包含了很多公共的信息。建议可以去看看这个包的pom.xml文件内容。
三、Maven导入JaCoCo
参考官网文档:https://www.jacoco.org/jacoco/trunk/doc/examples/build/pom.xml
其实有没有发现,前文有提及这个文档。对的,官方是有特意提供了Maven构建里使用JaCoCo的示例。
作为初学者,只需要直接引入jacoco-maven-plugin这个插件就好了
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
</plugin>
我们还可以对其做一些配置(这里只列举几个,具体的可配置项在前文jacoco-maven-plugin中已有展示)。
注意:有些配置是有默认值的,实在不懂的话,还是不要乱改动为好。
1、prepare-agent
地址:https://www.jacoco.org/jacoco/trunk/doc/prepare-agent-mojo.html
作用:准备指向 JaCoCo 运行时代理的属性,该属性也可以作为 VM 参数传递给受测应用程序。其实就是配置agent的属性,为等下运行测试用例的检测做准备。
注:需要版本>=0.5.3
注:默认情况下绑定到Maven生命周期阶段的initialize(Maven生命周期可参考官方的文档《Lifecycles Reference》)
使用示例:
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<!--
字段的值应该是使用标准通配符语法相对于目录target/classes/的已编译类的类路径(而不是程序包名称)
不配置就是全部类
* Match zero or more characters
** Match zero or more directories
? Match a single character
-->
<includes>
com/basic/happytest/modules/jaCoCo/*
</includes>
<destFile><!-- 这里其实是默认值 -->
${project.build.directory}/jacoco.exec
</destFile>
<skip>
false
</skip>
</configuration>
</execution>
2、report
地址:https://www.jacoco.org/jacoco/trunk/doc/report-mojo.html
作用:为单个项目的测试创建代码覆盖率报告,支持多种格式(HTML、XML 和 CSV)。如果是集成测试之类的情况,则用的是其他配置。(其实就是去加载agent生成的jacoco.exec文件,然后创建出来可阅读的格式的报告文件)
注:需要版本>=0.5.3
注:默认情况下绑定到Maven生命周期阶段的verify
使用示例:
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<outputDirectory> <!-- 默认值 -->
${project.reporting.outputDirectory}/jacoco
</outputDirectory>
<dataFile> <!-- 默认值 -->
${project.build.directory}/jacoco.exec
</dataFile>
<!--
指定只准备生成符合匹配规则的内容的报告
不指定的话默认报告包含全部类(无视surefire、prepare-agent等其他的限制范围)
-->
<includes>
com/basic/happytest/modules/jaCoCo/*
</includes>
<footer>
Footer text used in HTML report pages.
</footer>
<formats>
HTML
</formats>
<outputEncoding> <!-- 默认值 -->
UTF-8
</outputEncoding>
<sourceEncoding> <!-- 默认值 -->
UTF-8
</sourceEncoding>
<title> <!-- 默认值 -->
${project.name}
</title>
</configuration>
</execution>
3、check
地址:https://www.jacoco.org/jacoco/trunk/doc/check-mojo.html
作用:检查最终的代码覆盖率是否到达了设定的指标。如果项目没有指标需求,可以不配置,也就等同于不做这个检查。
注:需要版本>=0.6.1
注:默认情况下绑定到Maven生命周期阶段的verify
使用示例:
<execution>
<id>jacoco-check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<includes> <!-- 只检查符合匹配规则的内容的项,不选的话会扫描全部,无视其他配置的限制范围 -->
com/basic/happytest/modules/jaCoCo/*
</includes>
<haltOnFailure> <!-- 默认值 -->
true
</haltOnFailure>
<rules>
<rule>
<element>CLASS</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
注:作者很好奇官网提及的elment中可选的类型里BUNDLE指的是什么,是OSGi 体系架构里的Bundle吗?(留待高人解答)
如果通过设置的指标,会在日志里显示:
4、instrument
地址:https://www.jacoco.org/jacoco/trunk/doc/instrument-mojo.html
专门的官方文档:https://www.jacoco.org/jacoco/trunk/doc/offline.html
作用:进行离线检测代码覆盖率,存在一些缺陷,只有特定情况下才用。官方建议常规情况下是使用runtime的方式,而不是这种方式。所以这里无示例。
四、生成报告
如果你的测试方法原先写得可能不那么规范,不是可以循环使用的,那么就需要利用maven-surefire-plugin这个插件来排除一些可能会出问题的测试类,或者包含进一些测试类。(当然,这是无可奈何的情况下的举措,实际上,就应该规范书写测试类,然后进行全项目的覆盖率检测)(不过这样操作,实际上JaCoCo的配置也要跟着限制范围,导致最终生成的报告只是项目里的某个部分的报告)
当然,上面提及的属于特殊情况。
正常情况下不用考虑。
操作的话,直接在IDEA里点击右侧的Maven标签,然后在打开的窗口里,点击clean,再点击verify(具体取决于你生成report配置绑定的maven的生命周期),就可以生成覆盖率报告了。
或者使用maven命令的方式,直接执行 mvn verify命令(具体取决于你生成report配置绑定的maven的生命周期),可以一次性执行所有的测试用例,同时也将生成覆盖率报告。
生成覆盖率报告的页面操作
生成的覆盖率报告位置,取决于前文提及的你在report的配置里,设置的报告输出目标位置
配置示例
打开文件目录,可以看到对应的文件夹
示例
进入文件夹后,可以看到一个index.html的文件,双击点开
文件夹内容示例
浏览器中,将显示覆盖率报告内容信息
浏览器显示示例
可以点击包名、类名、方法,一步步进入到对应页面,可以看更详细的覆盖情况的信息
示例
五、未生成报告问题解决
1、情况:跳过了测试
不能跳过测试,跳过测试的话,自然就没覆盖率报告了。
配置实例
跳过测试的情况下,执行日志里可以看到:
日志示例
2、情况:执行没到生成报告的生命周期
JaCoCo每个步骤都默认绑定到了Maven的生命周期中的一个阶段(或者你在配置里面改了绑定的阶段位置),如果你执行的Maven命令没到达这个阶段,就不会执行这个步骤。
例如按照下图的配置,若执行的是mvn test,由于生命周期没到达verify这个阶段,就不会执行report、jacoco-check这两个步骤,除非换成执行mvn verify或者把repot、jacoco-check改成绑定到test这个阶段上,这样就能正常生成报告了。
配置示例
3、情况:生成报告了,但是又没完全生成报告
显示内容:No class files specified.
这种情况其实可以通过执行日志来查看具体是什么情况。
一般是JaCoCo的各个步骤都正常进行,但是你配置了includes和excludes后,导致最终没有符合匹配的项,于是就没有Class可以进行分析,所以就显示这个结果。
打开报告后的页面示例
这种情况下,执行日志可以看到:
六、覆盖率报告解读
官网对覆盖率数据介绍的地址:https://www.jacoco.org/jacoco/trunk/doc/counters.html
图源自官网
官网其实已经讲得很详细了,也没什么需要补充的内容。
七、扩展
1、 IDEA中jacoco.exec的使用
在IDEA中,点击上方菜单项的Run,然后在打开的面板中选择Show Coverage Data。
在弹出的页面中,选择“+”
此时,需要找到生成的jacoco.exec文件的路径,选中它,点击“OK”。
勾选jacoco.exec,并点击Show selected。
此时,IDEA的项目页面就会出现覆盖率的数据信息,如下图所示。
若要关闭显示覆盖率信息,则可以再次点击Run,然后点击Hide coverage,就可以了。
2、完整的一个JaCoCo的POM示例配置(本文覆盖率报告基于的配置)
注意:本配置仅供参考,建议阅读本文,同时基于代码上的相关注释来进行自定义修改。
<!-- 一个用于mvn生命周期的测试阶段的插件,可以通过一些参数设置方便的在testNG或junit下对测试阶段进行自定义 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<!--
默认情况下,Surefire 插件将自动包含具有以下通配符模式的所有测试类:
"**/Test*.java"- 包括其所有子目录和所有以“Test”开头的 Java 文件名。
"**/*Test.java"- 包括其所有子目录和所有以“Test”结尾的 Java 文件名。
"**/*Tests.java"- 包括其所有子目录和所有以“Tests”结尾的 Java 文件名。
"**/*TestCase.java"- 包括其所有子目录和所有以“TestCase”结尾的 Java 文件名。
如果测试类不遵循默认通配符模式,则才需要额外包含以下文件
-->
<includes>
JaCoCoInstanceT.java
</includes>
<excludes>
<exclude>**/Test*.java</exclude>
<exclude>**/*Test.java</exclude>
<exclude>**/*Tests.java</exclude>
<exclude>**/*TestCase.java</exclude>
</excludes>
<!-- 注意,如果想要输出代码覆盖率报告,则此处应该为false -->
<!-- 跳过测试阶段 -->
<skipTests>false</skipTests>
<!-- 参数 forkCount 定义 Surefire 为执行测试而同时生成的 JVM 进程的最大数量 -->
<!-- JaCoCo要求不能为0,此处设置的其实是默认值 -->
<forkCount>1</forkCount>
<!-- JaCoCo要求不能是Never,此处设置的其实是默认值 -->
<forkMode>once</forkMode>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<id>prepare-agent</id> <!-- 唯一标识这个特定的执行。这主要用于在需要的地方引用或区分多个执行。 -->
<phase>initialize</phase> <!-- 定义应该在哪个生命周期阶段执行该插件的目标 -->
<goals> <!-- 列出应该执行的目标 -->
<goal>prepare-agent</goal>
</goals>
<configuration>
<!--
字段的值应该是使用标准通配符语法相对于目录target/classes/的已编译类的类路径(而不是程序包名称)
* Match zero or more characters
** Match zero or more directories
? Match a single character
-->
<includes> <!-- 只准备扫描符合匹配规则的内容 -->
com/basic/happytest/modules/jaCoCo/*
</includes>
<destFile><!-- 这里其实是默认值 -->
${project.build.directory}/jacoco.exec
</destFile>
<skip> <!-- 此处该配置使用的是默认值 -->
false
</skip>
</configuration>
</execution>
<execution>
<id>merge</id>
<phase>generate-resources</phase>
<goals>
<goal>merge</goal>
</goals>
<configuration>
<fileSets> <!-- 此处该配置使用的是默认值 -->
<fileSet>
<directory>${project.build.directory}</directory>
<includes>
<include>*.exec</include>
</includes>
</fileSet>
</fileSets>
<skip> <!-- 此处该配置使用的是默认值 -->
false
</skip>
</configuration>
</execution>
<execution>
<id>report</id>
<phase>verify</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.directory}/jacoco
</outputDirectory>
<dataFile> <!-- 默认值 -->
${project.build.directory}/jacoco.exec
</dataFile>
<!--
指定只准备生成符合匹配规则的内容的报告
不指定的话默认报告包含全部类(无视surefire、prepare-agent等其他的限制范围)
-->
<includes>
com/basic/happytest/modules/jaCoCo/*
</includes>
<footer> <!-- 会显示在报告页面的最下面 -->
Footer text used in HTML report pages.
</footer>
<formats>
HTML
</formats>
<outputEncoding> <!-- 默认值 -->
UTF-8
</outputEncoding>
<sourceEncoding> <!-- 默认值 -->
UTF-8
</sourceEncoding>
<title> <!-- 默认值 -->
${project.name}
</title>
</configuration>
</execution>
<execution>
<id>jacoco-check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<includes> <!-- 只检查符合匹配规则的内容的项,不选的话会扫描全部,无视其他配置的限制范围 -->
com/basic/happytest/modules/jaCoCo/*
</includes>
<haltOnFailure> <!-- 默认值 -->
true
</haltOnFailure>
<rules>
<rule>
<element>CLASS</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>