拾运这轮测试,是我这段时间做的最累、也最出活儿的一轮。项目本身不算大,就是一个典型的三端同城货运调度平台:用户端下单、司机端抢单、运营后台管单。但正因为“三方角色+实时调度+资金结算”都挤在一个系统里,测试的复杂度一下子被拉起来了。尤其是回归阶段临近收尾时,整个测试组都在反复确认同一件事:抢单并发、支付回调、结算金额这几个老问题,到底稳了没有。
这篇复盘就当作是把拾运的测试过程完整记录一遍。我不打算把它写成一份干巴巴的验收文档,而是把测试范围怎么定的、环境怎么搭的、用例怎么设计的、压测怎么跑的、问题是怎么一个个揪出来的,全部摊开讲清楚。不管是刚接手项目的测试新人,还是准备给自家系统做一轮完整测试的团队,里面的大多数思路和操作你都可以直接拿过去参考。
1. 项目背景与测试范围界定
1.1 拾运项目到底在测什么
“拾运”这个名字听起来像个小工具,实际上它是一个完整的同城货运业务系统。货主在用户端发起订单,填写起止地点、货物类型、期望车型;系统根据距离和车型计算预估价格,订单进入公共订单池;司机在司机端刷单、抢单,抢单成功后开始取货、运输、送达;最后货主确认完成,平台抽佣,剩余运费进入司机账户。运营后台则负责司机入驻审核、订单人工干预、异常投诉处理、价格策略配置。
这轮测试的目标非常明确:上线前的回归验收。开发侧已经完成大部分功能开发,并且在上一轮测试中修掉了一批功能问题。这次不只是简单验证“修好了没有”,而是要站在上线角度确认系统整体能不能用、并发撑不撑得住、核心资金链路有没有隐患。所以我跟产品、开发对齐后,把测试范围收敛成四条主线:
- 订单主流程:下单、支付、抢单、取货、送达、结算全链路。
- 资金安全:支付回调、金额计算、账户余额、佣金抽成、结算明细。
- 并发场景:多人抢同一单、批量下单、热点司机被频繁派单。
- 消息与状态一致性:订单状态流转、司机端推送通知、异常超时补偿。
至于司机入驻资料审核、发票申请、投诉工单这类低频功能,这轮只做冒烟检查,不做深度回归。明确说一句:测试范围如果一开始不卡死,后面一定会被各种边角功能拖着跑,等到版本要发的时候才发现最核心的流程还没测透。
1.2 为什么这轮测试以回归验收为主
拾运的上一个里程碑版本已经做过一轮相对完整的功能测试,当时的结论是“功能可用,但性能风险未验证,资金链路需要重点盯”。这轮之所以被定位成回归验收,是因为中间经历了一次比较大的技术架构调整:订单服务从单体应用拆出来了,支付回调改成了异步消息处理,抢单逻辑引入了分布式锁。这三处改动几乎动摇了整个系统的地基,如果回归不彻底,上线后出问题的概率非常高。
另外还有一个现实约束:上线日期已经拍死,留给测试的时间只有不到两周。在这种时间压力下,不可能面面俱到,必须用“风险优先”的思路来排测试优先级。功能测试部分,我们主要围绕改动点做回归,而不是把全部用例重新跑一遍;性能测试部分,则集中打在订单查询和抢单这两个最容易出问题的接口上。这个选择事后证明是对的:真正严重的几个缺陷,全部集中在并发抢单和资金计算里,而这两块恰好是我们重点投入的部分。
2. 测试环境与数据准备
2.1 环境拓扑与部署版本
测试环境是搭建在内部机房的两台虚拟机上,配置都不高,每台4核8G,但在压测阶段这个问题反而暴露了价值:因为配置比生产低,压测结果会更早地暴露瓶颈,方便开发在低成本条件下先做一轮优化。具体部署结构大概是这样:
- 两台应用服务器,Nginx负载均衡,后端服务使用JDK 17,Spring Boot应用。
- 数据库MySQL 8.0,主从部署,测试库只用了主库。
- Redis 6.x,负责用户会话、订单缓存、分布式锁实现。
- RocketMQ 4.9,承载支付回调、订单状态变更消息。
这里要强调一个容易被忽略的点:测试环境部署版本务必跟生产保持一致。拾运的支付回调曾经在开发环境用了一个新版本的RocketMQ客户端,结果连测试环境的MQ旧集群一直报错,查了半天才发现是版本不一致导致的兼容性问题。后来我们定了一条规矩:环境部署单必须列清楚每个服务的版本号、分支号、构建时间,测试开始前先核对一遍。
2.2 测试数据造数方案
测试数据这块,一开始差点翻车。拾运的核心业务跟地理位置强相关,如果订单数据没有真实坐标体系,司机端刷单列表就会乱成一团。我们用两类方式造数:一类是直接写SQL插入历史订单和账户数据,另一类通过接口批量生成实时订单。
具体做法是,先用脚本在数据库里插入10万条历史订单,随机分布在城市的主要城区网格内,保证订单归属司机、结算状态都覆盖完成、已取消、退款中等各种情况。然后通过下单接口创建2000多笔动态订单,用来模拟当天活跃订单池。司机端账号准备了50个,分布在城市各个区域,确保不同位置的司机刷到的订单列表不一样。
最后还需要做一件事:Redis缓存预热。很多订单列表和司机状态是从Redis读取的,如果不预热,第一波压测请求会大量穿透到数据库,导致响应时间虚高,和真实运行状态完全不是一回事。我们用一段简单的脚本跑了一遍热点数据预热,把常用地理位置缓存、司机在线状态缓存全部提前装配好。
2.3 测试工具链选型
功能测试阶段,我们用Postman做接口调试,用Apifox管理接口用例集合,团队内部协作起来比较方便。到了性能压测阶段,工具直接锁定Jmeter 5.6.3。选它不是因为别的工具不好,而是因为三点:团队已有脚本资产基本都是Jmeter格式,不需要重写;Jmeter的图形化界面做采样器配置比较直观,新人上手快;压测结果可以用命令行直接生成HTML报告,方便我把报告打包发给项目组。
关于Jmeter 5.6.3,有一个小提醒:这个版本的插件生态已经很成熟了,但千万别一上来就装一堆扩展插件。真正核心的压测能力,线程组、HTTP请求、CSV参数化、聚合报告、HTML报告生成,这些内置功能已经全都覆盖了。插件装多了,一来脚本分发到别的机器时可能因为缺插件跑不起来,二来报告里的指标会变得混乱。
3. 功能测试设计与执行实录
3.1 核心流程测试:下单、支付、派单、取货、送达
功能测试的主线是业务全链路,我最常用的方法不是单个接口点对点测试,而是模拟真实用户把整条链路走通。拿拾运来说,最核心的一条链路是:货主下单 → 系统估价 → 支付运费 → 订单进入司机端列表 → 司机抢单 → 取货 → 送达 → 货主确认 → 平台抽佣 → 司机结算到账。
这串流程看着简单,实际操作起来每一步都有容易翻车的点。支付环节我们专门测试了支付成功后回调延迟、回调重复通知、支付金额与订单金额不一致三种场景。尤其是回调重复通知,这是资金类系统最容易出问题的地方,必须保证接口是幂等的,否则一笔订单被通知两次入账,账就平不了。实测场景中,我们通过模拟平台重复推送同一笔支付回调,确认订单状态和账户余额没有变化,这才放下心来。
抢单环节也是重灾区。我们同时准备了两台手机模拟司机端,一个订单刚进入司机端列表,立刻用两个司机账号同时点击抢单。第一轮测试时系统居然给两个司机都返回了抢单成功,订单状态却只绑定了一个司机ID,这在业务上就是极其严重的并发问题。开发看到这个问题后,才在抢单接口里补上了基于订单ID的分布式锁。后面我又用10个司机同时抢一单跑了20轮,确认只有一人能抢到,才把这条用例标记为通过。
取货和送达阶段,重点测的不是按钮能不能点,而是状态流转和消息通知。司机点击“已取货”以后,货主端是否能实时看到状态变化;司机到达目的地点击“送达”后,订单是否进入了待确认状态。这些细节看起来不起眼,但用户感知极强。我们在测试中还发现,司机端App在弱网环境下点击“送达”会出现超时重试,重试又可能触发两次状态变更事件,导致订单状态出现短暂混乱。这个问题的根源是客户端缺少防重控制,后来让开发在前端加了按钮重复点击禁用,后端又加了状态机校验,双保险才算稳住。
3.2 边界与异常场景专项
边界测试特别能体现测试人员的功底。拾运的计价逻辑里有一个很典型的边界条件:同城货运按里程计价,但里程有最低消费,同时距离过近或过远都可能走特殊计算分支。我们把起终点距离设置成0米、1米、500米、10公里、50公里,分别验证价格计算和文案提示,结果真发现了一个问题:当起终点完全一样时,前端居然能正常下单,系统按最低消费收了钱,但实际上司机根本无法接这种订单。后来产品确认,这种单子应该直接提示“起终点不能相同”。这个边界需求在原需求文档里是没有写的,靠的就是测试对业务的理解。
异常场景主要围绕支付超时、订单取消、司机取消、货主取消几种组合展开。我们设置了这样的用例矩阵:
- 支付超时:用户进入支付页但一直没有支付,订单超过15分钟自动取消。
- 支付成功后取消:货主取消订单,运费按规则退款,司机端收到取消通知。
- 司机抢单后取消:司机取消订单,订单重新回到公共订单池,但不允许同一司机再次抢同一单。
这几个场景组合起来,光是订单状态流转的预期结果就整理了满满两页。测试执行的时候,我要求测试人员每跑完一个组合,必须截图保存订单状态和账户流水,作为回归依据。因为这类问题往往不是单点测试能发现的,而是状态组合下的脏数据积累。
3.3 兼容性与UI走查
拾运这轮兼容性测试没有铺开做全矩阵,因为时间不允许。我们选了覆盖用户量最大的几种设备组合:iOS 15和iOS 16两个版本、Android 10和Android 13两个版本,外加微信内嵌浏览器打开H5页面。重点验证下单流程、支付流程、订单详情页在几种组合下的展示效果。
UI走查时几个页面出现了一些小问题,比如司机端抢单按钮在部分安卓机型上文案显示不全,订单列表的时间格式在iOS上显示成12小时制,跟产品设计稿不一致。这类问题一般不会阻塞上线,但会在后续版本修复,我统一整理进了遗留缺陷列表,打了P3优先级。
关于UI测试,我个人有个经验:不要过度依赖自动化截图对比。真正有效的做法是让测试人员像真实用户一样去操作,而不是只看像素级差异。拾运的司机端有一个“听单”语音播报功能,自动化工具根本测不出语音是否正常播放,只能靠人来听。这轮测试里我们就实打实坐在工位上,戴着耳机听了好几轮语音播报,才确认不同订单类型的播报内容没有串台。
4. 性能压测与JMeter报告解读
4.1 压测场景设计:先定目标,再定场景
性能测试最忌讳一上来就开工具跑,压之前得先想清楚:到底要验证什么?拾运压测的核心目标有三个:一是验证司机端订单列表查询接口在300并发下响应时间是否可接受;二是验证抢单接口在200并发下能否保证不重复派单、不超时;三是验证下单支付链路在Mock支付网关的前提下,整体吞吐量能不能撑住3000单/小时。
围绕目标,我设计了三个压测场景:基础单接口压测、混合链路压测、峰值突发压测。基础单接口压测用来定位单个接口的瓶颈;混合链路压测模拟真实业务比例,比如查询订单列表、抢单、确认送达、查询余额按照7:2:1的比例混合;峰值突发压测则模拟运营活动推送给司机后,瞬间大量司机同时刷单的冲击。
4.2 Jmeter 5.6.3脚本关键配置
Jmeter脚本的配置细节决定了压测结果的可信度。我在脚本里做了这几个关键设置:
线程组方面,按场景分别设置了不同配置。峰值突发场景用的是“同时启动”模式,配合同步定时器让所有线程在同一时间点发出请求,模拟瞬间高并发。如果不加同步定时器,Jmeter的线程启动会有时间差,根本制造不出“瞬间冲击”的效果。
参数化方面,用CSV Data Set Config加载司机账号和订单ID列表,避免多线程复用一个账号导致数据冲突。这里特别提醒:CSV文件里的数据量最好大于最大并发数的两倍,否则后面的线程会循环使用同一批数据,造成人为的数据竞争。
监听器方面,我在脚本里加了一个聚合报告,同时在命令行压测时用-l参数保存了JTL原始结果文件。后续用Jmeter 5.6.3生成HTML报告的时候,这个JTL文件就是数据源。
这里贴一下我实际使用的生成HTML报告命令:
jmeter -n -t pickup_test_plan.jmx -l pickup_results.jtl -e -o pickup_html_report压测结束后,直接打开pickup_html_report目录下的index.html,就能看到完整的响应时间分布、吞吐量、错误率图表,比在GUI界面里看聚合报告直观得多。Release 5.6.3 版本生成的报告里多了不少统计维度,尤其是百分位响应时间(p50、p90、p95、p99)的展示,比老版本好用了不少,对定位性能瓶颈很有参考价值。
4.3 压测过程实录与指标达标情况
先跑的是订单列表查询接口,300并发持续压了10分钟。第一次跑出来的结果让人不太满意:接口平均响应时间超过2秒,p95接近4秒,错误率到了1.2%。开发第一时间怀疑是不是数据库慢查询,我们把压测结果导出后,结合MySQL慢查询日志一看,发现问题出在一个order_info表关联user_address表查起终点详情的SQL上,数据量大之后没有走索引,回表次数高得吓人。开发加了联合索引并优化了查询字段后,第二轮压测平均响应时间降到480ms,p95降到1.1秒,错误率清零。
再看抢单接口,200并发下跑混合链路场景,第一轮的结果触目惊心:接口在极高并发下报错率高达6%,而且日志里出现了多个司机抢单成功的记录。这个问题与功能测试阶段发现的重复派单问题同源,都是分布式锁没有覆盖到所有路径。开发修复锁逻辑后,抢单接口在200并发下的错误率降到0.2%,p99响应时间在900ms以内。
下面这个表格整理了三轮压测的关键数据,作为最终报告的一部分:
| 压测场景 | 并发数 | 平均响应时间 | p95响应时间 | 错误率 | 吞吐量(TPS) |
|---|---|---|---|---|---|
| 订单列表查询(优化前) | 300 | 2012ms | 3987ms | 1.2% | 138 |
| 订单列表查询(优化后) | 300 | 478ms | 1120ms | 0% | 226 |
| 抢单接口(修复前) | 200 | 836ms | 1512ms | 6% | 96 |
| 抢单接口(修复后) | 200 | 412ms | 890ms | 0.2% | 162 |
4.4 报告生成与瓶颈定位方法
压测跑完,别急着把Jmeter报告甩到群里就没下文了。我对报告的分析思路是:先看整体健康度,再看错误请求,最后看分位耗时。整体健康度看的是吞吐量和错误率,判断系统是不是处于可用状态;错误请求看的是报错堆栈和响应体,定位是超时、连接池耗尽还是业务断言失败;分位耗时看的是p90、p99之间的差距,拉得越大,说明系统抖动越严重。
举个具体例子:订单列表查询接口优化后,p50响应时间是350ms,但p99达到2.1秒,这个差距非常不正常。后来跟踪发现,部分请求命中了没有做缓存热点的偏远区域订单数据,Redis缓存全部穿透到数据库。这类问题只看平均值是发现不了的,必须结合报告里的“响应时间百分位图”才能定位。
Jmeter 5.6.3生成的HTML报告里,我重点看三个页面:Dashboard、Statistics、Response Time Percentiles。Dashboard里的吞吐量、错误率、响应时间汇总能快速判断整体情况;Statistics里每个Sampler独立的平均耗时和错误数可以做接口排名;Response Time Percentiles图则用来识别长尾请求。把这几个图整理成一轮“性能健康周报”,比单纯贴聚合报告截图更有说服力。
5. 缺陷分析:这轮测试揪出的典型问题
5.1 并发抢单场景下的重复派单
这是整个测试周期里最严重的问题,没有之一。具体表现是:多个司机同时抢同一订单时,系统出现了抢单成功但订单绑定司机不一致的情况,甚至出现了一个订单被两个司机同时抢到,业务上完全不可接受。
我用Jmeter开启200个并发线程,全部请求同一个订单ID的抢单接口,第一轮压测的聚合报告里错误率飙到6%,同时数据库中查出两条抢单成功记录。开发排查后发现,抢单接口虽然有分布式锁,但锁的粒度加在了司机维度而不是订单维度。同一个司机同时抢不同订单不会冲突,但两个司机抢同一订单时,锁根本不起作用,业务判断与状态更新之间出现了时间窗。修复方式是锁的key改成order:pickup:{orderId},同时在订单表中用update ... where status = '待抢单'做CAS校验,事务内二次确认,彻底堵住了并发漏洞。
5.2 金额精度与计算口径不一致
结算模块的问题比较隐蔽,不是一眼能看出来的。我让测试人员故意用“非整除”的金额组合:订单金额100.01元,平台佣金比例18%,测试实际结算到司机账户的金额是否准确。第一遍测下来,司机端显示到账82.01元,但运营后台的结算报表里写的是82.00元,两边差了0.01元。
排查后发现问题出在代码实现上:司机端结算用的接口是double类型计算,运营报表那边用的是BigDecimal,两边的舍入方式不一样,导致同一条订单在两端显示不一致。虽然金额只差一分钱,但对资金系统来说这是绝对不能接受的。最终统一改成BigDecimal,并且一旦计算时发现除不尽,统一使用“银行家舍入”,还加了一条自动化断言,保证两端金额偏差永远为0。
5.3 消息推送延迟与补偿机制缺失
司机端抢单成功、订单状态变更这些消息,系统里都用了RocketMQ异步通知。我在测试中模拟了一个极端场景:手动停掉其中一个MQ消费者节点,然后连续触发一批订单状态变更。恢复消费者后,发现部分消息丢失,司机端没有收到取消通知,订单状态跟司机App界面显示不一致。
更深层次的问题在于系统缺少消息补偿机制。订单状态变更后,如果MQ发送失败,没有兜底任务去重新扫描未通知的订单。这个缺陷影响面很大,尤其是司机已经出发去取货但货主取消了订单,如果司机收不到消息,会造成极差的用户体验甚至投诉。开发给关键消息链路加了一张消息发送记录表,发送失败后由定时任务重推,连续失败超过三次再转人工告警处理。
5.4 缺陷统计与分布
整个测试周期,拾运项目一共提了47个缺陷,按严重程度和模块分类如下:
| 严重程度 | 数量 | 说明 |
|---|---|---|
| P0致命 | 2 | 重复派单、金额精度不一致 |
| P1严重 | 9 | 消息丢失、订单状态错乱、支付重复入账风险 |
| P2一般 | 21 | 页面样式、文案、弱网提示不友好 |
| P3建议 | 15 | 交互优化、性能优化建议 |
缺陷分布方向上,订单模块和结算模块合计占到了61.7%,印证了测试重点放在这两个方向的判断是正确的。虽然建议类缺陷数量不少,但大部分被推迟到了下一版本,只有P0和P1的11个缺陷作为上线前必须修复项,全部完成回归验证。
6. 测试结论、风险与遗留事项
6.1 质量评估结论
从功能、性能、稳定性三个维度总结,拾运系统当前状态如下:
功能层面,订单、支付、结算、后台管理四条核心链路都已完成回归,P0、P1缺陷全部关闭并复测通过,P2缺陷不阻塞上线。性能层面,订单列表查询、抢单接口在同一并发条件下的响应时间达标,错误率控制在指标范围内。稳定性层面,混合链路压测持续运行1小时,无内存泄漏迹象,JVM堆使用率稳定在60%以下。
整体评估结论是:拾运系统基本达到上线条件,可以进入限量发布阶段。但是,必须强调的是,这个结论建立在“并发抢单修复后必须保持监控”的前提上。如果上线后实际订单量远超压测峰值,不排除还会出现新的性能瓶颈。
6.2 上线前必须处理的风险项
虽然测试结论给了“可以上线”,我还是在报告里明确标注了几个风险项,要求项目组上线时重点关注。
第一,Redis在抢单链路里承担了分布式锁和订单缓存两个职责,如果Redis发生故障,整个抢单业务会瞬间瘫痪。当前系统没有做Redis高可用切换演练,建议上线前安排一次主从切换演练,确认切换时间内业务的行为是否符合预期。
第二,支付回调依赖RocketMQ,一旦MQ集群压力过大,消息积压会导致订单状态长时间不更新,货主端和司机端看到的单量状态会不一致。测试期间我们压过单机MQ到3万消息量时出现明显积压,线上消息量如果超过这个数,需要有扩容预案。
第三,司机端App覆盖的机型有限,测试阶段只验证了主流的iOS和Android版本,一些低端机型上是否会有卡顿、页面崩溃,目前没有数据支撑。建议上线后关注司机端异常上报数据,如果某些机型的崩溃率超过阈值,需要紧急介入处理。
6.3 下一个版本需要补的测试功课
这一轮测试因为时间紧张,有几块内容没有做深,我在复盘里主动列了出来,留给下一版本补上。
兼容性测试矩阵需要扩大,尤其是Android低端机的覆盖,至少要补足10到15款主流低端机型。自动化回归能力也需要沉淀,目前功能测试还是手工为主,下一轮回归时希望把订单主链路的关键接口用例做成自动化回归集,至少保证发版前能全量跑一遍。性能测试方面,建议引入持续性能监控,而不是只在版本发布前做一次压测。埋点上报、接口响应时间、错误率这些指标应该有一个日常巡检看板,这样性能退化能在早期被发现,而不是等上线后用户投诉了才定位。
7. 项目复盘:测试团队自己的功课
7.1 如果重来一次,我会改什么
测试工作做完了,回过头看,整个过程里最值得改进的是“测试数据准备”和“开发联调介入点”两个环节。
测试数据这块,我们第一周花了太多时间在手工造数上。如果重来一次,我会在项目启动的第一天就写好数据初始化脚本,并且要求开发提供批量创建订单与司机的内部接口。现在这些SQL脚本已经沉淀在测试工程里了,下一轮测试可以节省至少两天的准备时间。
开发联调介入点也一样。抢单并发问题其实完全可以在联调阶段就暴露出来,但因为联调时通常用Postman一个一个发请求,很难手动制造并发冲突。如果联调一开始就让开发把Jmeter脚本跑起来,用20个并发做冒烟,很多大问题能提前暴露,也省得后面回归时再来复查。
7.2 关于Jmeter报告的几个实用技巧
最后分享几个这轮测试里实际验证过的Jmeter 5.6.3使用技巧,都是踩过坑换来的。
第一,压测一定用命令行模式跑,不要开着GUI压测。GUI跑大量并发不仅结果不准,自己电脑内存也会被吃满。命令行模式只要一条命令就能跑完并生成报告。
第二,保存JTL结果文件的路径不要放在中文目录下,Jmeter解析时会报错,这个坑很浪费排查时间。
第三,压测前先在测试环境跑一个1分钟的小并发预热,让JIT编译完成、连接池初始化完成,然后再开始正式压测,否则前几秒的数据会虚高或者虚低,干扰分析。
第四,默认的聚合报告只看一个平均值,信息量远远不够。真正做性能定位时,一定要打开响应时间百分位图表、吞吐量曲线和错误率图,三个维度配合着看,才能准确判断瓶颈到底在系统层面还是业务逻辑层面。
做一个合格的测试报告,不是把用例执行记录堆出来就完事了。它得能回答三个问题:系统能不能上线,上线后会遇到什么,出了问题怎么补救。拾运这轮测试,我们最终给出了“有条件上线”的结论,也把各种潜在风险一个一个拆出来、晾在桌面上。后续如果真的因为并发问题出事故,至少我们知道该从哪里下手去查。这就是测试报告最大的价值。