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一共有四个枚举参数,分别是
- tcpserver: agent作为tcp服务,执行的数据将写入到服务中,可随时dump
- tcpclient: agent作为client,可以向指定的ip和端口推送执行数据
- file: jvm停止时会自动生成dump文件
- 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,实现以下目标
- 生成exec文件
- 把jenkins中打包的admin.jar解压到指定目录,用于访问class
- 根据exec+class+源码生成html报告
- 使用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脚本工程,生成新的报告
第三个例子
目标
- 上面生成的报告里有很多包的覆盖率数据我们并不关心,需要关注的是业务层的类,需要屏蔽掉哪些无关的包
- 上面我们部署的应用只有一个服务,而实际的应用往往有很多服务,需要在每一个服务启动时都带上agent并且能把覆盖率数据合并到一起
方案
- 报告中不展示无关的包,没有找到合适的方法,不过在启动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参数效果:展示的包没有减少,但无关的包不再统计覆盖率数据
- 多服务的问题,由于我手头没有多服务项目,只能使用重复启动多个相同服务的方式模拟。在网上找到了两种方案分别是
- agent启动时将output设置为tcpserver和tcpclient,采用主从模式启动。生成dump文件时只需要针对server即可,只生成一个dump文件
经多次尝试,此方案没有成功,按主从模式启动agent后,生成dump文件时无法连接tcpserver,留待以后再尝试 - 每个服务单独起一个agent server,生成多个dump文件,然后再用jacoco提供的merge工具合并到一起,生成一个总的dump
- agent启动时将output设置为tcpserver和tcpclient,采用主从模式启动。生成dump文件时只需要针对server即可,只生成一个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
后续工作
- 与实际项目结合,推动落地。人员培训,优先级高
- 增量覆盖率研究,优先级低
存在问题
- 人员能力问题:要掌握分析代码覆盖率的能力, 有下列两个核心的需求。
- 要能掌握对应开发语言的核心语法和框架,能看懂代码的逻辑。而测试团队普遍代码能力较弱,能达成此要求需要一定的时间培训学习和实践,和自身的努力也分不开。
- 要熟悉自身所测试项目的代码结构,能快速的根据所测试的业务模块定位到对应的代码,并能理解代码的业务场景。要达成这一目标还需要研发人员的配合与培训。
- 可靠性问题:根据代码的覆盖率补充用例后,路径和分支全覆盖并不等于没有BUG。即覆盖率作为准入标准或卡点是不完全可靠的,它可以起到一个锦上添花的作用。
- 成本问题:每轮测试后分析覆盖数据、补充用例的时间成本问题。