Jacoco 测试覆盖率探索

Jacoco 测试覆盖率探索实践

Jacoco 说明

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

  • 原理 :java程序在装载类时, 利用asm框架修改对应的class文件进行插桩,加入计数器实现对代码执行情况的统计。当目标应用程序运行时,JaCoCo Agent 会在代码执行的关键点收集覆盖率信息,例如方法的入口和出口,以及条件分支的执行情况。
  • 模式:
    • on the fly 模式 :通过Java agent在JVM执行字节码之前动态对其进行修改, 适用于测试环境,方便在开发和测试过程中了解代码的覆盖情况。
    • offline 模式 : 在Java程序字节码文件(.class文件)生成之前进行修改,是通过分析已经存在的类文件和源代码来收集代码覆盖率信息,而不需要实际运行代码。
      由于 on the fly 模式更适用于测试环境的采集分析,下面的介绍都基于这种模式操作

第一个例子

前置条件

  • 后端服务jar包 : 当前我们已经有一个由springboot框架打包的后台服务程序,名称为【admin.jar】 ,可通过 java -jar admin.jar 的方式来启动后端服务。
  • class文件夹 :解压admin.jar把其中的class文件夹解压出来
  • 源代码: admin.jar对应的源代码
  • 配套前端 : admin.jar有一个配套的前端工程,可通过本地部署的方式运行。(用来通过UI方式执行测试)
  • jacoco工具 : 已下载jacoco-0.8.9并解压缩到指定目录

步骤一

首先,使用以下命令启动admin服务以及javaagent,注意在启动服务应用时,需要加上jacoco的agent参数,例如

java -javaagent:.\jacoco-0.8.9\lib\jacocoagent.jar=includes=*,output=tcpserver,port=2019,address=localhost -jar admin.jar
  • includes=*:这表示 JaCoCo 应该监控所有的类。* 通配符表示所有类。
  • output=tcpserver:这指定了 JaCoCo 的输出方式。tcpserver 表示 JaCoCo 将以 TCP 服务器模式输出覆盖率数据。

output一共有四个枚举参数,分别是

  1. tcpserver: agent作为tcp服务,执行的数据将写入到服务中,可随时dump
  2. tcpclient: agent作为client,可以向指定的ip和端口推送执行数据
  3. file: jvm停止时会自动生成dump文件
  4. none :不提供任何输出
  • port=2019:这指定了 JaCoCo 服务器的端口号。在这里,JaCoCo 将在本地主机的 2019 端口上运行。
  • address=localhost:这指定了 JaCoCo 服务器的地址为本地主机。

步骤二

打开配套前端,手工执行测试操作(略)

步骤三

使用以下命令将覆盖率信息dump到指定文件

java -jar "${jacocoHome}/lib/jacococli.jar" dump --address ${serverIp} --port ${serverPort} --destfile "${retHome}/tmp/jacoco3.exec"
  • dump:这是 Jacoco CLI 工具的一个子命令,用于执行覆盖率数据的转储操作。
  • –address ${serverIp}:这个参数指定了远程服务器的 IP 地址,用于告诉 Jacoco CLI 工具要从哪个服务器获取覆盖率信息。(在这里就是 localhost)
  • –port ${serverPort}:这个参数指定了远程服务器的端口,与 --address 一起用于确定连接的服务器。(在这里就是2019)
  • –destfile “${retHome}/tmp/jacoco3.exec”:这个参数指定了覆盖率数据的目标文件。

步骤四

根据生成的exec文件和class文件+源代码生成HTML格式的覆盖率报告

java -Dfile.encoding=UTF-8 -jar "${jacocoHome}/lib/jacococli.jar" report "${retHome}/tmp/jacoco3.exec" --classfiles "${packageHome}/${jarName}/BOOT-INF/classes" --sourcefiles "${jksHome}/${jksJobName}/${jarName}/src/main/java" --html "${reportHome}/${reportDirectory}" 
  • report:这是 JaCoCo CLI 工具的一个子命令,用于生成覆盖率报告
  • –classfiles : 这个参数指定了包含编译后的类文件的路径
  • –sourcefiles : 这个参数指定了源代码文件的路径。JaCoCo 将使用这些源代码文件来生成报告,以便在报告中显示源代码和覆盖率信息的对应关系。
  • –html : 这个参数指定了生成的报告的输出路径和格式。

步骤五

查看生成的覆盖率报告,根据覆盖率信息调整测试用例
在这里插入图片描述

  • Missed Instructions Cov(未执行指令):表示在测试过程中未执行的指令数量。这是一个基本的覆盖率度量,指示了在代码中哪些部分没有被执行到。
  • Missed Branches Cov(未执行分支):表示在测试过程中未执行的分支数量。这指示了在代码中哪些条件分支没有被执行到(异常处理不算做分支)
  • Cxty (Complexity)(圈复杂度):表示相应部分的代码复杂度,这是一个相对于代码结构复杂度的度量。简单的说就是为了覆盖所有路径,所需要执行单元测试数量,圈复杂度大说明程序代码可能质量低且难于测试和维护
  • Missed Lines(未执行行):行覆盖率,只要本行有一条指令被执行,则本行则被标记为被执行
  • Missed Methods(未执行方法):方法覆盖率,任何非抽象的方法,只要有一条指令被执行,则该方法被计为被执行
  • Missed Classes: 类覆盖率,所有类,包括接口,只要其中有一个方法被执行,则标记为被执行。注意:构造函数和静态代码块也算作方法

我们点击查看 yss.acs.testplatform.pages.service.tidu 这个包中的 ApiTestHelperImpl 类中的funcEval方法的详情,这是一个用来模拟自动化测试框架中内置函数执行结果的方法,逻辑比较简单
在这里插入图片描述
点击 方法名进入代码染色详情页
在这里插入图片描述

  • 钻石代表分支覆盖情况
    • 红色钻石:这一行没有分支被执行
    • 黄色钻石:这一行中只有部分分支被执行
    • 绿色钻石:这一行的所有分支都被执行
  • 背景颜色代表指令覆盖率
    • 红色背景:这一行并没有任何指令被执行
    • 黄色背景:这一行的部分指令被执行
    • 绿色背景:这一行的所有指令都被执行了

可以看到 funcname == “yesterdady” 时输入参数为空的分支没有被覆盖,所以我们打开前端页面再执行一次yesterday函数,输入参数为空
在这里插入图片描述

重复上面的步骤三和步骤四,重新生成exec文件和html报告,再次打开报告查看该方法的详细信息,可以看到该行代码背景已经变为绿色
在这里插入图片描述
由此可以看出我们可以根据jacoco的覆盖率报告来对测试用例查漏补缺。

第二个例子

目标

  • 前面的例子中,我们都是在本地启动的服务以及生成覆盖率报告的,实际工作中这些工作都需要在服务器中通过持续集成工具来完成。所以我们需要把上面的操作集成到持续集成中。
  • 最后生成的HTML报告部署到nginx上可以直接通过浏览器访问
  • 报告生成完成后推送钉钉消息,带上报告连接

前置条件

  • 部署jenkins服务
  • admin.jar 对应前后端代码都在gitlab上维护,并且在jenkins上实现了自动部署
  • 把 jacoco工具包上传到jenkins所在服务器中
  • 服务器IP为192.168.0.177

第一步

  • 首先需要改造一下admin.jar在jenkins中的部署脚本,把jacoco的agent加进去,这样在部署时就会启动agent
# 省略前面的打包和文件处理步骤
# 改造前
# nohup java -jar admin.jar >/dev/null 2>&1 &
# 改造后
nohup java -javaagent:./jacoco-0.8.9/lib/jacocoagent.jar=includes=*,output=tcpserver,port=2019,address=192.168.0.177 -jar admin.jar >/dev/null 2>&1 &

第二步

访问服务器上部署的系统,执行测试

第三步

在jenkins中新建一个自由风格项目,构建操作就是执行一个shell,实现以下目标

  1. 生成exec文件
  2. 把jenkins中打包的admin.jar解压到指定目录,用于访问class
  3. 根据exec+class+源码生成html报告
  4. 使用curl发送钉钉消息
current_datetime=$(date "+%Y%m%d_%H%M%S")
#jacocoagent的服务器IP和端口
serverIp="192.168.0.177"
serverPort="2019"
#发布report的nginx端口
nginxPort="9529"

# 生成exec文件及jenkins映射存储目录的根目录
retHome="/home/yxd"
# jenkins工作空间目录
jksHome="${retHome}/jenkins-data/workspace"
#nginx的发布目录
reportHome="/usr/share/nginx/jacocoReport"
#部署的jar包名称
jarName="admin"
# jenkins项目名称
jksJobName="TesterTools"
#jenkins存放jar的目录
packageHome="${jksHome}/${jksJobName}/${jarName}/package"
#jacoco工具所在目录
jacocoHome="/root/shell/jacoco-0.8.9"
#生成的jacoco报告路径
reportDirectory="jacocoReport_${jksJobName}_${current_datetime}"

# shell 生成exec文件
java -jar "${jacocoHome}/lib/jacococli.jar" dump --address ${serverIp} --port ${serverPort} --destfile "${retHome}/tmp/jacoco3.exec"
echo "jacoco3.exec文件生成完毕!"

# 删除Class文件所在的目录,确保jar包与class文件版本一致
rm -rf "${packageHome}/${jarName}"

# 检查 rm 命令的退出状态 
if [ $? -eq 0 ]; then
   echo "删除${jarName}目录完毕!"
else
   echo "删除${jarName}目录失败!"
   exit 1  # 退出脚本,表示错误状态
fi

# 重新解压Jar包
unzip "${packageHome}/${jarName}.jar" -d "${packageHome}/${jarName}"

# 检查 unzip 命令的退出状态
if [ $? -eq 0 ]; then
   echo "解压${jarName}.jar完成!"
else
   echo "解压${jarName}.jar失败!"
   exit 1  # 退出脚本,表示错误状态
fi

# 生成report
echo "开始生成jacoco报告"
java -Dfile.encoding=UTF-8 -jar "${jacocoHome}/lib/jacococli.jar" report "${retHome}/tmp/jacoco3.exec" --classfiles "${packageHome}/${jarName}/BOOT-INF/classes" --sourcefiles "${jksHome}/${jksJobName}/${jarName}/src/main/java" --html "${reportHome}/${reportDirectory}" 
echo "Jacoco报告已生成 :${reportDirectory}"

token="249601cf4516f89ea86706bfeea3f19989c1b80eeb72******770d45e1a2bd86" 
url="https://oapi.dingtalk.com/robot/send?access_token=${token}"

curl --request POST \
 --url $url \
 --header 'Accept: application/json' \
 --header 'Content-Type: application/json' \
 --header 'Authorization:Basic eWFuZ3hpYW9kb25nOnRpZ******3MzE=' \
 --data '{
   "msgtype": "link",
   "link": {
       "messageUrl": "http://'"${serverIp}"':'"${nginxPort}"'/'"${reportDirectory}"'/",
       "picUrl":"",
       "title": "【'"${jksJobName}"'】覆盖率报告-【'"${current_datetime}"'】",
       "text": "[通知]测试覆盖率报告生成成功!"
   }
}'

第四步

在这里插入图片描述
点击钉钉中的报告通知查看报告详情,后续调整了用例之后可以再次执行shell脚本工程,生成新的报告

第三个例子

目标

  1. 上面生成的报告里有很多包的覆盖率数据我们并不关心,需要关注的是业务层的类,需要屏蔽掉哪些无关的包
  2. 上面我们部署的应用只有一个服务,而实际的应用往往有很多服务,需要在每一个服务启动时都带上agent并且能把覆盖率数据合并到一起

方案

  1. 报告中不展示无关的包,没有找到合适的方法,不过在启动agent时,可以设置不采集这些包的覆盖率信息

比如我们只需要采集service包中java类的覆盖率信息,可以在includes中设置包路径=yss.acs.testplatform.pages.service.*

java -javaagent:.\jacoco-0.8.9\lib\jacocoagent.jar=includes=yss.acs.testplatform.pages.service.*,output=tcpserver,port=2019,address=192.168.0.177 -jar admin.jar

includes参数效果:展示的包没有减少,但无关的包不再统计覆盖率数据

includes效果展示

  1. 多服务的问题,由于我手头没有多服务项目,只能使用重复启动多个相同服务的方式模拟。在网上找到了两种方案分别是
    1. agent启动时将output设置为tcpserver和tcpclient,采用主从模式启动。生成dump文件时只需要针对server即可,只生成一个dump文件
      经多次尝试,此方案没有成功,按主从模式启动agent后,生成dump文件时无法连接tcpserver,留待以后再尝试
    2. 每个服务单独起一个agent server,生成多个dump文件,然后再用jacoco提供的merge工具合并到一起,生成一个总的dump
  • 使用tcpserver分别在0.177和localhost启动两个admin服务和对应的前端
java -javaagent:.\jacoco-0.8.9\lib\jacocoagent.jar=includes=yss.acs.testplatform.pages.service.*,output=tcpserver,port=2019,address=192.168.0.177 -jar admin.jar
java -javaagent:.\jacoco-0.8.9\lib\jacocoagent.jar=includes=yss.acs.testplatform.pages.service.*,output=tcpserver,port=2019,address=localhost -jar admin.jar
  • 在两个环境中分别执行不同的操作后,生成各自的exec文件
  • 再合并两个exec文件,生成jacoco3_meraged.exec
java -jar .\jacoco-0.8.9\lib\jacococli.jar merge .\report\jacoco3.exec .\report\jacoco3-1.exec --destfile .\report\jacoco3_meraged.exec
  • 使用合并后的exec文件生成报告
java -Dfile.encoding=UTF-8 -jar ".\jacoco-0.8.9\lib\jacococli.jar" report .\report\jacoco3_meraged.exec --classfiles D:\tmp\admin\BOOT-INF\classes --sourcefiles D:\Project\test-platform\admin\src\main\java --html D:\tmp\report\report_html3

后续工作

  • 与实际项目结合,推动落地。人员培训,优先级高
  • 增量覆盖率研究,优先级低

存在问题

  • 人员能力问题:要掌握分析代码覆盖率的能力, 有下列两个核心的需求。
    1. 要能掌握对应开发语言的核心语法和框架,能看懂代码的逻辑。而测试团队普遍代码能力较弱,能达成此要求需要一定的时间培训学习和实践,和自身的努力也分不开。
    2. 要熟悉自身所测试项目的代码结构,能快速的根据所测试的业务模块定位到对应的代码,并能理解代码的业务场景。要达成这一目标还需要研发人员的配合与培训。
  • 可靠性问题:根据代码的覆盖率补充用例后,路径和分支全覆盖并不等于没有BUG。即覆盖率作为准入标准或卡点是不完全可靠的,它可以起到一个锦上添花的作用。
  • 成本问题:每轮测试后分析覆盖数据、补充用例的时间成本问题。
  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值