jacoco框架学习与使用
一、jaCoco介绍
1. JaCoCo是什么?
JaCoCo(Java Code Coverage)是一个用于 Java 项目 的 代码覆盖率工具。分析代码执行情况,评估测试用例对代码的覆盖程度,生成覆盖率报告。
JaCoCo 支持不同级别的覆盖率评估:
- 行覆盖率(Line Coverage):每一行代码是否被执行。
- 方法覆盖率(Method Coverage):每个方法是否被调用。
- 类覆盖率(Class Coverage):每个类是否被加载。
- 分支覆盖率(Branch Coverage):每个分支判断是否被执行(例如 if-else 语句)
2. 问题与优势
-
测试案例的设计凭经验,当研发一个新功能时,经常对测试场景估计不足,到上线后发现bug;
-
开发经常做一些需求之外的代码变更(代码小范围内重构或在开发过程中发现小缺陷随手改掉),导致测试任务无法测试到对应的场景,引起线上问题;
-
对测试效果无法量化考核,导致测试工作的质量无法进一步提升。
优势 | 说明 |
---|---|
代码覆盖率可视化 | JaCoCo 帮助开发者直观地查看代码的测试覆盖情况 |
提高代码质量 | 通过评估测试覆盖率,帮助识别潜在的代码缺陷和测试盲点 |
代码审查与评估 | 提供覆盖率报告,支持团队评估项目质量并指导开发工作 |
确保测试用例全面性 | 确保测试用例覆盖大部分代码路径,而不仅仅是执行某些代码 |
开源轻量 | 性能良好,运行时开销很小,尤其是对于大型项目; |
易于集成 | JaCoCo 提供了 CLI 工具、Maven 插件、ant 插件,以及与 Jenkins 等 CI 工具的集成支持。 |
3. jacoco原理
JaCoCo支持几种不同的方法来收集覆盖信息,对于每种方法,由不同技术实现的,下图橙色路径部分是JaCoCo 推荐使用的方式,即通过On-The-Fly方式收集覆盖率信息:
通过上图我们知道,JaCoCo 是通过对Java字节码(Byte Code)插入探针的方式来收集覆盖率信息的,探针是可以插入现有指令之间的附加指令。它们不会改变方法的行为,但会记录它们已被执行的事实。
下面以一段简单的 程序为例进行说明:
这段代码经过Java编译以后转化为以下字节码:
因为Java 字节码指令的线性序列,控制流是通过条件或无条件指令实现跳转的,跳转目标在技术上是相对于目标指令的偏移量。这个跟大学学习的汇编指令的跳转方式类似,为了更好的可读性,使用符号标签 (L1,L2 ) 代替实际的指令地址。
上图中橙色的部分为插入的探针,理论上我们可以在控制流图的每个边缘插入一个探针,由于探针实现本身需要一些字节码指令,这将会使类文件的大小增加数倍;幸运的是,这不是必需的,实际上我们只需要根据方法的控制流为每个方法插入几个探针。例如,没有任何分支的方法只需要一个探针。
如果已经执行了探测,我们就知道相应的边已经被访问过。从这条边我们可以得出结论到其他前面的节点和边:
- 如果一条边被访问过,我们就知道这条边的源节点已经被执行了;
- 如果一个节点已经被执行并且该节点是只有一条边的目标,我们知道这条边已经被访问过。
如果我们在正确的位置有探针,递归地应用这些规则可以确定方法的所有指令的执行状态,探针只是需要在控制流边缘插入的一小段附加指令。
4. 流程:
JaCoCo 的工作原理基于 字节码增强 和 插桩技术,主要步骤如下:
- 字节码插桩:
- JaCoCo 在程序编译后对 Java 字节码 进行修改(插桩)。这意味着 JaCoCo 会修改
.class
文件中的字节码,在方法执行前后插入额外的代码,用于记录代码是否被执行过。 - 当你运行测试时,JaCoCo 会跟踪代码的执行,并记录 哪些方法、行或分支 被执行。
- JaCoCo 在程序编译后对 Java 字节码 进行修改(插桩)。这意味着 JaCoCo 会修改
- 运行时数据收集:
- JaCoCo 插桩的代码在运行时会跟踪哪些部分的代码被执行。当你运行 Java 程序时,JaCoCo 会生成一个 覆盖率数据文件(例如
.exec
文件),它记录了程序执行过程中哪些行代码被触发。
- JaCoCo 插桩的代码在运行时会跟踪哪些部分的代码被执行。当你运行 Java 程序时,JaCoCo 会生成一个 覆盖率数据文件(例如
- 覆盖率报告生成:
- 运行完测试用例后,JaCoCo 会根据收集到的数据生成 代码覆盖率报告。这个报告可以是 HTML、XML 或 CSV 格式,展示每个类、方法、行的覆盖情况。
- 报告能够清晰地显示哪些代码行被测试覆盖,哪些没有被执行。通过这些信息,开发者可以做出改进和优化,确保测试的充分性。
二、单元测试集成jacoco
开发人员进行单元测试的时候,可以通过集成jacoco插件的形式,生成单元测试的代码覆盖率,针对未覆盖的代码编写更多的测试用例,以确保代码的全面覆盖,从而提高代码质量和可靠性。
本示例是基于springBoot+maven的组合。
1. 引入jaCoco的maven插件
<build>
<plugins>
<plugin>
<!-- JaCoCo的 Maven 插件,用于生成代码覆盖率报告 -->
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.6</version>
<executions>
<!-- 第一个任务prepare-agent编译阶段执行,它负责准备 JaCoCo 代理,以便在测试运行时收集覆盖率数据。-->
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- 第二个任务在 test 阶段执行,目标是 report。这个执行阶段生成代码覆盖率报告-->
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<!-- 非必需:配置的规则检查覆盖率是否达标。如果覆盖率不满足配置的要求,构建将失败。-->
<execution>
<id>jacoco-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>PACKAGE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.0</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<!-- 这个插件用于执行 Maven 构建中的测试 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<!--该配置设定了单元测试执行结束后,Maven等待进程退出的最长时间(单位:秒)-->
<forkedProcessExitTimeoutInSeconds>60</forkedProcessExitTimeoutInSeconds>
<!-- 设置fork的进程数,1 表示只启动一个进程执行测试。如果你需要并行执行测试,可以增加该值 -->
<forkCount>1</forkCount>
</configuration>
</plugin>
</plugins>
</build>
上述配置引入两个maven插件jacoco-maven-plugin(jacoco插件相关)、maven-surefire-plugin(执行maven构建中的测试)
2. 编写单元测试类
//服务类
@Slf4j
@Service
public class CustomerApplication {
@Resource
private CrmChargeUserMapper crmChargeUserMapper;
@Resource
private CrmBusEncryptMapper crmBusEncryptMapper;
public void getAreaInfo() {
String areaNames = "北京";
TableFieldEnum customer = TableFieldEnum.CUST_CUST_INFO;
DynamicDataSource.setDataSourceType(customer.getDataBaseName());
//有if 分支 所以这个接口会走if里面的,外边的不会走,则编写单元测试执行后代码覆盖率为50%
if(StringUtils.isBlank(areaNames)){
List<PhoneEncryptPO> phoneEncryptPOS = crmBusEncryptMapper.selectDataListByIds(customer.getDataBaseName(), customer.getTableName()
, customer.getIdField(), customer.getPhoneField(), Lists.newArrayList(2035279L));
log.info("查询客户信息:{}", phoneEncryptPOS);
return;
}
List<AreaInfo> areaInfos = crmChargeUserMapper.selectAreaList(Lists.newArrayList(areaNames));
log.info("查询区域信息:{}", areaInfos);
}
}
//单元测试
@Slf4j
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = ToolApplication.class)
@WebAppConfiguration
public class CrmBusEncryptServiceTest {
@Resource
private CustomerApplication customerApplication;
//单元测试
@Test
public void getAreaInfo() {
customerApplication.getAreaInfo();
}
}
3. 执行测试流程
通过idea的maven管理插件、或者mvn test命令执行完毕,会生成对应的xxx.exec文件(可以通过jacococli 将exec文件转换成html查看覆盖率)
其中会生成jacoco.exec,html的覆盖率报告在target/site目录下。
覆盖率报告如下:和上面单元测试类预估的覆盖率一致为50%
三、项目整合jacoco
上面我们说了单元测试如何整合jacoco,但是不是每个开发都会写单元测试(不会为整个项目都写测试用例)。测试人员需要针对项目http接口进行覆盖率的分析。所以此章节项目整合jacoco框架。
主要流程:
- 下载安装jacoco
- java项目启用配置的javagent,并请求api接口
- 下载覆盖率报告并生成覆盖率报告
1. 安装jacoco
里面详细罗列了所有按需下载的jacoco 版本,下载并解压到指定目录,此处和单元测试使用jacoco版本一致使用0.8.6
#下载并解压(第二种下载方式)
#下载jacoco
wget http://search.maven.org/remotecontent?filepath=org/jacoco/jacoco/0.8.6/jacoco-0.8.6.zip
#解压
unzip jacoco-0.8.6.zip
软件包功能
jar包 | 功能 | 关键说明 |
---|---|---|
jacocoagent.jar | 作为Java代理通过JVM的javaagent 参数注入到运行中,实时对字节码进行插桩(On-the-fly Instrumentation),跟踪代码执行路径。 | 接口测试或需要长期监控覆盖率的场景。 |
jacococli.jar | 提供命令行接口(CLI),支持覆盖率数据的导出、合并、报告生成及离线插桩操作。 | dump:导出实时覆盖率数据到.exec文件 report:解析文件,结合源码和编译文件生成HTML/XML/CSV格式的覆盖率报告 merge:合并多个 .exec 文件,支持多轮测试数据的整合分析 |
jacocoant.jar | 将JaCoCo与Apache Ant构建工具深度集成,通过自定义Ant任务自动化覆盖率分析流程。 | 类似于maven、gradle构建工具 |
工作流程
- 启动应用:通过
-javaagent:jacocoagent.jar
参数附加代理,启动TCP Server。 - 执行测试:运行单元测试或手动测试,覆盖目标代码。
- 导出数据:使用
jacococli.jar dump
命令从TCP Server导出.exec
文件。 - 生成报告:通过
jacococli.jar report
或Ant任务生成可视化报告。 - 分析优化:根据报告识别未覆盖代码,迭代改进测试用例。
2、配置jacoco
在项目启动脚本(strat.sh)或者本地idea的vm options 添加代理配置
#-javaagent:[jacoco软件安装路径/]jacocoagent.jar=includes=包名,output=tcpserver,port=端口,address=ip
javaagent:/usr/local/jacoco/lib/jacocoagent.jar=includes=*
,output=tcpserver,port=6200,address=127.0.0.1,append=true
参数名称 | 作用 | 示例值 | 注意事项 |
---|---|---|---|
includes | 指定需要收集覆盖率的类(支持通配符匹配) | includes=com.xieqx.* | 默认值为 * ,表示包含所有类 |
excludes | 排除特定类或包(避免收集无关代码) | excludes=com.xieqx.test.* | - |
output | 设置覆盖率数据输出方式 | output=tcpserver | 可选值:file (写入文件)、tcpserver (启动TCP服务器)、tcpclient 、none |
append | 控制数据追加行为 | append=true | true :追加数据到现有文件;false :覆盖文件(默认) |
address | 指定TCP服务器绑定的网络接口 | address=* | * 表示监听所有接口;生产环境建议限制为特定IP |
port | 设置TCP服务器监听的端口号 | port=6200 | 确保端口未被占用,且与客户端配置一致 |
dumponexit | JVM退出时自动导出覆盖率数据 | dumponexit=true | 默认值为 false ,适用于短期运行的测试任务 |
outputfile | 指定输出文件路径(仅在 output=file 模式有效) | outputfile=./coverage.exec | - |
classdumpdir | 转储代理处理的类文件到指定目录(用于调试动态生成的类) | classdumpdir=/path/to/classes | - |
jmx | 启用JMX接口(允许远程监控和管理覆盖率数据) | jmx=true | - |
sessionid | 为覆盖率数据分配唯一会话ID(便于多实例区分数据) | sessionid=12345 | - |
exclclassloader | 排除特定类加载器加载的类(避免框架内部类干扰统计) |
3、生成覆盖率报告
项目启动后,请求项目相关api接口。即会产生覆盖率报告。后面我们需要将对应的覆盖率dump(下载)下来(xxx.exec文件),并report生成html(xml/csv)覆盖率报告。
dump覆盖率报告
# 注意因为上面启动时tcpserver 且设置了允许远程ip端口,所以处理需要和上面ip端口保持一致
java -jar /usr/local/jacoco/lib/jacococli.jar dump --address 127.0.0.1 --port 6200 --destfile /Users/xie qx/Documents/jacoco-demo.exec
参数 | 描述 |
---|---|
address | 指定JaCoCo代理运行的TCP服务器的地址。例如,--address 127.0.0.1 。 |
port | 指定JaCoCo代理运行的TCP服务器的端口号。例如,--port 6300 。 |
destfile | 指定生成的覆盖率数据文件的路径和名称。例如,--destfile ./jacoco.exec 。 |
reset | 可选参数,表示在dump操作后是否重置覆盖率数据。如果设置为true ,则重置数据;如果设置为false 或不设置,则不重置数据。 |
retry | 可选参数,表示在连接失败时重试的次数。例如,--retry 10 表示重试10次。 |
quiet | 可选参数,表示是否以静默模式运行,不输出详细信息。 |
上述dump操作会在我们destfile对应目录中生成jacoco-demo.exec。
report覆盖率报告
dump下来的覆盖率只是exec没法查看具体的信息。所以需要使用report导出为html(xml/csv)等进行查看
java -jar ${jacoco的安装路径}/lib/jacococli.jar
report ${生成的覆盖率文件路径}/jacoco-demo.exec
--classfiles /{项目路径}/target/classes/com
--sourcefiles /{项目路径}/src/main/java
--html /Users/yiche/Documents/report
参数 | 描述 |
---|---|
<execfiles> | 指定一个或多个包含覆盖率数据的执行文件(.exec 文件),多个文件之间用逗号分隔。 |
--classfiles <path> | 指定包含被测试类文件(.class 文件)的目录或文件路径。JaCoCo 将从这个路径加载对应的类文件以生成覆盖率报告。 |
--sourcefiles <path> | 指定源代码文件的目录或文件路径,以便在生成的报告中附加源代码信息。 |
--html <dir> | 指定生成 HTML 格式报告的输出目录。 |
--xml <file> | 指定生成 XML 格式报告的输出文件路径。 |
--csv <file> | 指定生成 CSV 格式报告的输出文件路径。 |
--title <title> | 设置生成报告的标题。 |
--name <name> | 设置生成报告的名称,用于标识不同的报告。 |
--filters <filter> | 用于指定过滤器,以排除某些特定的类或包。过滤器使用 Java 的包命名规则,可以使用星号(*)作为通配符。 |
--encoding <charset> | 指定生成报告时使用的字符编码,例如 UTF-8 。 |
--tabwith <n> | 设置生成报告时使用的制表位宽度。 |
--quiet | 可选参数,表示以静默模式运行,不输出详细信息。 |
--help | 显示 report 命令的帮助信息。 |