拓冰建站拓冰建站
首页 / 资讯中心 / 正文

手动测试覆盖率实践:用Jacoco和Jenkins量化你的每一次点击

做测试这行大家应该都有过这种体验功能点测了一轮又一轮Bug也提了不少但真要问一句“你这轮测试到底把哪些代码跑到了哪些类压根没碰过”大部分人回答不上来。覆盖率、覆盖率工具——比如Jacoco——在大伙印象里多半和自动化测试、CI流水线绑在一起总觉得那是测开同学玩的东西手动测试用不上。结果就是项目里自动化测试覆盖率数字挺好看可真正在线上手点出来的那一大堆场景从来没有被“标记”过代码里哪些分支是活的、哪些早就死在犄角旮旯全靠测试同学的经验和第六感。这篇文章就是要把Jacoco拉到手动测试的场景里来聊聊怎么用它的Java Agent、CLI、Maven插件配合IDEA、Jenkins这些常用工具把手动测试的足迹也做成一份能看的覆盖率报告。适合谁在一线点点点的功能测试、兼职测试的开发、以及想把测试过程量化出来的测开同学都适用。读完之后你至少能手写一条启动命令、看懂一份exec文件、在Jenkins里把“手动选模块构建”和“定时全量构建”两种模式配到同一个Job里。1. 手动测试为什么也要用量化工具盯覆盖率1.1 手动测试覆盖率的尴尬现状先说一个扎心的现实。绝大多数项目的手动测试覆盖面是靠测试用例设计保证的。用例设计得好不好依赖个人经验、对需求的熟悉程度、以及上一轮回归时踩过的坑。但用例设计得再细执行完之后你依然回答不了三个问题第一这次的代码改动有没有被完全执行到第二有没有某些类、某些分支从上线到现在压根就没被任何一次操作触发过第三不同模块的测试深度差异到底有多大是A模块测了十几遍、B模块只过了一遍冒烟还是其实都差不多。我自己待过的项目里最典型的场景是一个服务十几个模块测试排期紧的时候大家优先测核心链路外圈功能“能点通就行”。等上线后出问题翻日志一查发现那个出问题的类代码覆盖率报表里是刺眼的红色。查来查去所有人都没在测试环境完整走过那条分支。从概率角度看这几乎是必然会发生的事——你不可能把一个几十万行代码的服务靠人肉记忆全部覆盖一遍。1.2 Jacoco解决的核心问题JacocoJava Code Coverage是个开源的覆盖率工具它的核心价值就一句话在字节码层面埋好探针记录哪些代码被执行过最后汇总成一份报告。它不关心你是用JUnit跑的单测、用Selenium跑的自动化、还是完全靠人手点的功能测试——只要你跑的是Java应用它就能记录“你的动作”真实覆盖到了哪一行字节码指令。放到手动测试的场景里我可以说Jacoco解决的不只是“覆盖率统计”这个工具题而是一个更实际的管理题它第一次让手动测试这件事有了“看得见”的物理证据。测试人员点完一轮功能后不再只能提交一份Excel格式、勾勾选选的用例执行表还能附带一份覆盖率报告里面清清楚楚显示哪些代码是被这批手工操作真实踩过、哪些代码这轮根本没进去。这个数据对测试排期、风险判断、开发自测都有直接价值。2. 动手前先搞清楚Jacoco的核心原理2.1 字节码插桩是怎么实现的Jacoco的底层原理是字节码插桩。Java代码编译成.class文件后里面是JVM能识别的字节码指令。Jacoco在类加载的时候或者构建期用ASM库往这些字节码里插入一些“探针”也就是一小段记录指令。程序跑起来之后每经过一个探针就会标记“这里被访问过”。打个不太严谨但好懂的比方你在一栋办公楼每个房间门口放一个打卡器员工每进一个房间就刷一下卡。到了晚上回收打卡记录你就能知道今天哪些房间有人进去过、哪些房间一天都没人碰。Jacoco就是这套访客系统探针就是打卡器exec文件就是晚上的打卡汇总。2.2 on-the-fly与offline两种模式Jacoco支持两种插桩模式理解它们的区别有助于你判断不同场景该用哪种。on-the-fly模式运行时插桩通过-javaagent参数在JVM启动时挂载一个agentagent在类加载过程中对字节码做动态插桩。这种方式最方便不需要修改构建产物改一下启动命令就能用也是手动测试场景里我用得最多的方式。offline模式构建期插桩在编译阶段直接对.class文件做插桩然后再打包。这种方式适用于不支持Java Agent的场景比如Android、某些不允许启动参数变更的部署平台。缺点是需要改构建流程、插桩后的class文件不能再直接用于生产稍微麻烦一点。手动测试场景优先选on-the-fly因为你要盯的是“现有环境里的真实服务”不想为了采集覆盖率专门打一个特殊包。2.3 五种覆盖率的含义Jacoco报告里有几种覆盖率指标手动测试前最好先搞清楚它们各自代表什么不然很容易被数字骗了。覆盖类型含义手动测试中最直观的参考价值指令覆盖Instruction字节码指令级覆盖反映代码整体有没有被执行到行覆盖Line源码级行是否有执行最常见的汇报指标分支覆盖Branchif/switch等分支是否全部走完评估“条件判断”测的是否全面方法覆盖Method方法是否被调用过快速定位完全没入口的方法类覆盖Class类是否被加载/实例化过判断项目中“孤立类”这五个指标里行覆盖最容易涨也最容易好看分支覆盖才是真正考验测试深度的。一个if-else语句如果测试用例只走到了if分支行覆盖可能照样是100%但分支覆盖只能算50%。手动测试时建议盯着分支覆盖看它能逼你多想想“反向条件”测没测。3. 本地手动测试覆盖率两种最快的接入方式3.1 IDEA内置Run with Coverage如果你平时直接在IDEA里启动Spring Boot应用然后手工点接口最省事的办法就是用IDEA自带的覆盖率运行功能。操作步骤很简单在启动类或者Application配置上选择“Run ... with Coverage”IDEA会自动用Jacoco把应用跑起来。等测试完、关掉应用编辑器左侧就会出现红黄绿三种颜色条颜色含义绿色代码行被执行过黄色代码行部分执行比如if只走了一个分支红色代码行没被执行过选中某个Java文件在左侧色条上点右键还能看到具体到每个方法、每条分支的执行次数。IDEA这种方式有俩特点一是方便零配置二是只适合本地开发自测产生的覆盖数据留在IDEA进程里不持久、不方便归档。你要是想把手动覆盖率的报告存下来跟团队共享或者跟后续的Jenkins任务关联起来就得用下面第二种方式。3.2 Java Agent方式启动应用我个人的习惯是需要留痕、需要出报告的手动测试一律用Java Agent方式启动。格式如下java -jar app.jar \ -javaagent:path/to/jacocoagent.jardestfile/data/jacoco/jacoco.exec,appendtrue,includescom.example.*几个关键参数必须拆开讲参数作用说明destfile覆盖数据的输出文件建议指定一个独立目录别放/tmpappend是否追加数据设为true可以叠加多轮测试设false会覆盖之前的数据includes/excludes类过滤规则控制哪些包/类纳入统计sessionid会话标识多轮测试区分来源按照项目实际情况我会把includes配成核心业务包的包名比如com.xx.order.*这样报告只聚焦业务代码不会把框架、第三方库的几十万个类全部统计进来报告看起来干净得多。启动完成之后正常做你的手工功能测试。等测完一轮用下面的命令把JVM内存里的覆盖数据“落盘”到本地java -jar cli.jar dump --address localhost --port 6300 --destfile jacoco_dump.exec这条命令走的是Jacoco agent内置的TCP输出协议。如果你在启动参数里没做特殊配置agent默认会在应用启动时监听一个端口通常是6300等待外部执行dump请求。我踩过的坑是agent挂在测试环境服务器上时一定要把--address配成服务器内网IP而不是127.0.0.1否则你在自己电脑上执行dump根本连不上。这个细节在下一节接着展开。4. 测试环境手动测试远程Agent与报告生成4.1 服务器部署时挂载Agent到了测试环境手动测试就不是自己电脑上点了而是要对着服务器上跑的服务点。这时必须在服务启动脚本里加agent参数。以Tomcat和Spring Boot可执行Jar为例Spring Boot Jar方式JAVA_OPTS$JAVA_OPTS -javaagent:/opt/jacoco/jacocoagent.jaroutputtcpserver,address0.0.0.0,port6300,appendtrue,includescom.example.*Tomcat方式修改setenv.sh如果不存在则新建放在Tomcat的bin目录下CATALINA_OPTS$CATALINA_OPTS -javaagent:/opt/jacoco/jacocoagent.jaroutputtcpserver,address0.0.0.0,port6300,appendtrue,includescom.example.*这两处的关键点是outputtcpserver和address0.0.0.0前者把覆盖率数据暴露成TCP服务后者允许外部IP连接。这样做的目的很简单你不需要登录服务器人工去拿exec文件只要从你本地或者Jenkins上执行一条dump命令就能把数据拉回来。4.2 dump与报告生成拿到exec文件之后下一步是生成HTML报告。方法一用CLI生成最通用java -jar cli.jar report jacoco_dump.exec \ --classfiles /path/to/classes \ --sourcefiles /path/to/src \ --html /path/to/report/html注意--classfiles要指向编译后的class文件目录如果你的服务是多模块项目就得分别把每个模块的class目录和源码目录都传进去。方法二用Maven插件生成适合项目里已有pom的场景mvn jacoco:report前提是在pom里配好jacoco-maven-plugin并把exec文件路径指到dump出来的位置。生成后的HTML报告页面表头一般包含元素包/类/方法、未覆盖指令、未覆盖分支、覆盖率百分比。点进某个包能看到更细的类级数据再点进类就能看到具体到源码行级别的红黄绿标注。4.3 多轮测试exec文件合并手动测试通常不是一次性做完的。今天测一轮支付明天测一轮退款想汇总成一份总报告就需要合并多个exec文件java -jar cli.jar merge jacoco_day1.exec jacoco_day2.exec jacoco_day3.exec --destfile jacoco_total.exec合并的逻辑是“只要任何一轮测过就算覆盖过”。实际操作中我习惯每次测试前把appendtrue配合sessionid参数设置好比如-javaagent:jacocoagent.jardestfile/data/jacoco/jacoco.exec,appendtrue,sessionidpaytest_20250101这样哪怕没做手动merge单份exec文件里也保留了多轮执行的累计数据。5. Jenkins场景手动选择模块构建定时构建的配置实录5.1 需求拆解两种触发方式怎么兼容把Jacoco落地到团队协作层面Jenkins是绕不开的一环。这里有一个网上经常搜到、也比较典型的组合需求同一个Jenkins任务既能手动触发时选择构建哪些模块又能在定时触发时按默认配置自动执行全量构建逻辑。先说需求拆解。手动选择模块本质是“参数化构建”——要让构建脚本拿到用户输入的模块列表然后动态拼Maven或Gradle的参数。定时触发本质是“用默认参数自动跑”——到了预定时间Jenkins给这个同参数任务一个缺省值默认全量。这两个场景塞进同一个Job核心就一件事让脚本对“参数为空”有合理处理。我采用的思路是加一个字符串参数MODULE_LIST默认为空。手动构建时测试人员往里填模块名比如order-service,user-service定时构建时走默认的空值脚本里判断为空就构建全部模块。5.2 参数化构建配置细节在Jenkins Job配置页的“General”部分勾选“参数化构建过程”添加一个String Parameter参数名默认值说明MODULE_LIST留空逗号分隔的模块名空表示全量构建然后在“构建”步骤里用Shell脚本做判断。以Maven多模块项目为例完整脚本大致像这样#!/bin/bash # 设置错误时退出 set -e # 定时触发传入为空时默认全量构建 if [ -z $MODULE_LIST ]; then echo MODULE_LIST为空执行全量构建 MODULES else # 将逗号分隔转成Maven的 -pl 参数 MODULES-pl $MODULE_LIST echo MODULE_LIST非空按指定模块构建$MODULES fi # 注意多模块项目只按 -pl 构建时如果模块间有依赖建议加上 -am同时构建依赖模块 mvn clean test $MODULES -am -DskipTestsfalse这段脚本有两个细节值得说明第一-amalso make参数很重要。比如你选了order-service但它依赖common-service的某些新改动如果没加-amMaven可能用了旧版本的依赖覆盖率数据也会失真。第二set -e要写成脚本开头。否则前面模块名拼错了Maven只会告警最后生成的覆盖率报告可能是不完整的。与自动构建的区别在于定时执行时不需要人工选模块所以默认值留空让脚本走“全量构建”分支即可。这样同一个Job就同时满足了两种触发方式。实际项目中我一般会再配置两个视图方便团队使用一个“手动测试专用”视图显示这个参数化Job另一个“持续集成”视图显示定时触发的Job。5.3 Jacoco在Jenkins中的两种集成为法方法一官方JaCoCo插件Jenkins插件中心有“JaCoCo”插件安装后可以直接读取exec文件并生成趋势图。这个插件的主要使用场景是配合自动化测试在构建结束后自动解析target/site/jacoco/jacoco.exec生成覆盖率变化曲线。优点是开箱即用适合流水线里跑单测缺点是不太灵活手动测试的数据文件位置、多模块的class路径它不一定能识别准确。方法二自定义构建步骤 CLI推荐我在处理手动测试覆盖率时更倾向在Jenkins的流水线里手写步骤应用启动时挂载agent已在第4节说明手动测试完成后执行一条Shell命令dump数据java -jar /opt/jacoco/cli.jar dump \ --address ${TEST_SERVER_IP} \ --port 6300 \ --destfile ${WORKSPACE}/jacoco_dump.exec调用Maven插件或CLI生成报告java -jar /opt/jacoco/cli.jar report ${WORKSPACE}/jacoco_dump.exec \ --classfiles ${WORKSPACE}/order-service/target/classes \ --sourcefiles ${WORKSPACE}/order-service/src/main/java \ --html ${WORKSPACE}/jacoco-report用Jenkins的“HTML Publisher”插件把报告展示到Job页面上。这个方法的好处是完全绕开了插件对自动化测试的假设手动测试的数据照样能进到项目统一的报告归档里。Jenkins里看到的覆盖率趋势才是“真实业务操作”的覆盖率而不是单测跑出来的覆盖率。6. 覆盖率报告怎么读别被行覆盖率骗了6.1 报告页面的阅读顺序拿到一份Jacoco HTML报告我看到很多人第一眼去看“所有包的总体覆盖率”那个数字然后就没有然后了。这个习惯很危险。Jacoco报告最重要的是“红色区域”也就是没覆盖到的部分。我个人的阅读顺序是先看包级列表哪个包整体覆盖度最低标注风险再点开低覆盖率的包看是哪些类拖了后腿点开具体类把红色行记录到一个“未覆盖清单”里找开发确认这些红色代码是历史遗留、没人会走到还是新改动没测到在很多项目中你会看到一个现象某些核心业务包覆盖率高达90%以上但配置类、工具类、某些兜底逻辑的覆盖率常年是0。后者虽然不起眼一旦被异常场景触发往往就是线上事故。6.2 从红色区域反推遗漏测试场景覆盖率报告最实用的用法是拿它反推测试场景设计。举个例子你测了一个订单创建接口报告显示OrderService.createOrder里有一个if分支是黄色的点开一看那行代码是if (order.getAmount() 0 order.getAmount() 10000) { // 正常金额逻辑 } else { // 超限逻辑或者异常金额逻辑 }说明你只测了正常金额没测超限金额和负金额。于是你补用例再执行再看覆盖率那个分支就变绿了。这样循环两三轮之后手动测试就不再是盲人摸象每轮测试前先定目标比如“这轮把order包的分支覆盖从60%推到80%”测完用报告验证、找缺口、补用例。这就是“结构覆盖率”对手动测试最有价值的地方——它把“覆盖率”从结果指标变成了测试过程的导航仪。关于报告里的那个“Missed Instructions”列也值得多说一句。它显示的是这个类/包/方法总共漏掉了多少条字节码指令数量越大说明风险敞口越大优先级应该排得更高。7. 常见问题与排查技巧实录7.1 exec文件丢失或数据被覆盖这是我遇到最多的问题。测试服务器上服务一重启之前累计的覆盖率数据全没了。原因是destfile指向了临时目录比如/tmp重启后被系统清掉或者启动参数里appendfalse每次启动都重新生成空文件。建议长期方案是给JaCoCo单独建一个持久化目录比如/data/jacoco/启动参数固定成appendtrue。如果担心数据无限膨胀可以在每轮版本测试结束后dump一份完整数据归档走再让运维重启时清空那个目录。7.2 端口冲突与Agent启动失败outputtcpserver模式下agent默认监听6300端口。如果服务器上有多个Java应用都要做覆盖率采集端口会冲突第二个应用直接启动失败。报错一般长这样Exception in thread main java.io.IOException: Address already in use: bind。解决办法给每个应用分配不同的端口同时用sessionid区分数据。或者一台机器只让一个关键应用开tcpserver其他应用用outputfile模式直接写本机文件然后定时用脚本统一收集。7.3 报告生成报错class文件路径不匹配用CLI生成报告时--classfiles路径的匹配是基于包名结构去查找的。如果你传的class目录不对经常遇到“Source file not found”或者报告里源码全是“Unknown”的情况。解决办法就是严格按照Maven编译产物目录来传# 多模块项目分别指定各个模块 --classfiles order-service/target/classes --classfiles common-service/target/classes还有一种容易遇到的坑本地编译的class版本和测试环境跑的class版本不一致。如果开发在本地改了代码测试环境跑的还是旧包dump出来的覆盖数据和源码行号对不上。我一般会在生成报告前先确认目标class文件的最后修改时间是不是测试环境部署时间吻合。7.4 排除策略基础设施代码别算在内手动测试覆盖率统计里最影响性能量和报告可读性的就是统计了太多无关类。依赖的DTO、实体类、配置类、生成代码这些东西你测多少次业务功能它们也未必全部“覆盖”但会把总体覆盖率拉低好几个百分点干扰真正的风险判断。建议在agent参数中把不需要统计的排除掉-javaagent:jacocoagent.jarincludescom.example.*,excludescom.example.dto.*:com.example.config.*,appendtrueexcludes用冒号分隔多个规则。颗粒度上我建议只要业务核心的service、controller这几个包纳进来就够用了。7.5 手动测试覆盖率低得吓人先别慌有朋友第一次给手动测试出覆盖率看到个位数直接慌了觉得这测试没法做了。实际上手动测试的覆盖率天然会比单测低这是正常的。单测会调用很多内部方法、私有分支而手动测试只能通过UI或接口去触发很多“内部工具方法”根本不会走到。这时候别急着追求高覆盖率先把“这次改动涉及的关键模块覆盖率”拉到一个合理水位。比如只改了一个订单查询功能那就只看订单相关服务的覆盖率有没有达到70%以上其他模块覆盖率低不影响本次风险判断。这样覆盖率就是工具而不是KPI绑架。8. 一点心得收尾用Jacoco查手动测试覆盖率这个习惯我坚持了两年多最大的变化是测试工作从“今天点完这些功能就完事”变成了“今天必须把报告里这几个红色方法想办法变成绿色”。这个转变不是靠责任心推动的是数据在推动的。几个小建议顺手送给要入坑的朋友第一次接入时不要贪多先找一个核心模块试点跑通agent、dump、报告生成这条链路再推广报告生成后记得在团队周会上花十分钟过一下“这次哪里没测到、为什么没测到”比单纯晒一个覆盖率数字有用得多另外agent参数里的appendtrue建议常开因为你永远不知道这轮手测完了下轮需不需要跟这轮数据合并对比。最后再说个实用小技巧如果你经常需要给不同环境出覆盖率报告可以给cli.jar写一个小脚本把dump和report两个命令连起来执行参数只留IP、项目名、版本三个输入。这样团队的测试同学不需要理解Jacoco内部细节拿到手就会用。覆盖率工具最终还是为人服务的用得顺手、看得懂、能指导下一步动作才是它真正的价值。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门