☰
庆典抽奖系统测试实战:Jmeter并发压测与Selenium自动化回归
2026/10/2 9:04:11 网站建设 项目流程

临近年会,行政那边突然提了个需求:做一个庆典抽奖助手。开发用Java两天把后端搭了起来,奖品管理、抽奖规则、中奖记录、大屏展示一应俱全。作为测试,我接到的任务是——在庆典现场几百号人同时按手机之前,把这套Java写的抽奖助手用Jmeter压到位,再用Selenium把核心UI流程自动化跑起来,确保活动现场不出幺蛾子。

这篇东西算是我的完整体验复盘,写给那些同样要接手活动类系统测试的同学。抽奖这类系统的测试和普通业务系统不太一样,它有一个特点:现场不能重来。线上出bug可以回滚,庆典现场出了问题,几百人盯着大屏,那是没法说"我们修一下再抽"的。所以测试的重点不是覆盖多少条用例,而是把最可能在现场爆发的风险提前摁死。

1. 抽奖系统上线前,我要验证什么

1.1 庆典场景的特殊性决定了测试边界

接到这个测试任务后的第一反应不是急着写用例,而是先弄清楚这个系统的使用场景到底长什么样。抽奖助手的使用高峰极其集中——庆典现场某个环节,主持人说"开始抽奖",全场几百人同时点屏幕。这个场景和电商秒杀高度相似,都是短时间内的流量尖峰,但比秒杀更棘手:秒杀失败可以提示"已抢光",抽奖如果没反应,现场气氛直接冷掉。

再去跟开发过了一遍功能清单,确认了核心功能是这些:

  • 管理员后台配置奖品:奖品名称、数量、中奖概率、抽奖轮次
  • 参与用户通过手机页面参与抽奖:输入手机号或工号验证身份后点击抽奖
  • 中奖记录实时落库,前端大屏滚动展示中奖名单
  • 管理员可查看、导出中奖记录

这里有个容易被忽视的点:抽奖助手的用户身份验证不能做得太重。庆典现场用户就是扫个码进来,如果还要注册、登录、填一堆资料,流程就断了。所以实际上系统用的是手机号+验证码的轻量校验,这在后面的性能和自动化测试里都有影响——每个用户身份的生成方式要贴近真实场景。

1.2 Java后端的功能点拆解与用例策略

功能用例的拆解我按"抽奖前、抽奖中、抽奖后"三段来分,比按模块拆更贴合现场逻辑。抽奖前要验证奖品配置的规则约束,比如奖品数量不能为负、概率之和不能超过100%、同一轮次不能重复抽;抽奖中要验证核心的抽奖算法,包括概率命中是否准确、库存扣减是否精确、并发请求下会不会超发;抽奖后要验证记录落库、大屏展示推送、管理员导出数据的准确性。

用例策略上,我优先把"现场最容易翻车"的场景排在了最前面。中等优先级的大概有二十多条,低优先级的比如后台样式展示这些,都排在了后面。后来事实证明这个排序是对的:真正出问题的恰恰就是并发下的库存准确性和重复点击这两个"现场高频场景"。

1.3 测试环境与数据准备的几个细节

测试环境直接借用了预发布环境,数据库是独立的MySQL实例,配置和线上基本一致,最后压测的数据才有效。数据准备这边做过一次返工,原因是刚开始准备的测试用户全是同一批手机号段,导致在按手机号做负载均衡的环节出现了单节点热点,后来重新生成了多号段数据才解决。

还有一个容易被忽略的细节是奖品数据的初始化。压测时如果奖品库存本来就设置得很大,比如10万件,那并发下的扣减压力根本体现不出来。我最终把核心压测奖品的库存设定在真实场景的量级——一等奖3件、二等奖10件、三等奖50件,这才暴露了后面要讲的高并发超发问题。

2. Jmeter压测从50并发加到500,抽奖接口发生了什么

2.1 接口梳理与Beanshell断言的取舍

抽奖场景的接口链路不算复杂,但压测不能只压抽奖那一个接口,否则结果没有说服力。我梳理出四条核心链路:身份验证、查询当前轮次与奖品信息、抽奖动作、查询中奖记录并展示。其中抽奖动作是最核心的接口,也是最容易出现并发问题的点。

在Jmeter里我用HTTP请求采样器来逐个模拟这些接口。身份验证这步需要动态参数,直接从CSV文件里读入预先生成的手机号;抽奖接口的请求体里有轮次ID和用户标识,轮次ID从查询接口的响应里提取。这里就是Jmeter关联的常规操作,用JSON提取器把上一个请求返回的轮次ID取出来,传给抽奖请求。

断言方面,我用了两种。一种是响应断言,直接校验HTTP状态码和返回的JSON里有没有"success"字段;另一种是Beanshell断言。很多人对Beanshell断言有点怵,其实它的价值在于能做复杂逻辑校验。比如抽奖接口返回的JSON里有一个"prizeLevel"字段,我需要断言"用户抽中了三等奖时,返回的奖品名必须等于预设值",这种跨字段的逻辑校验用响应断言很难写,用Beanshell就很灵活:

String response = new String(prev.getResponseData(), "UTF-8"); if (response.contains("\"prizeLevel\":3")) { if (!response.contains("三等奖")) { Failure = true; FailureMessage = "奖品等级与奖品名称不匹配"; } }

这个断言在后面的问题排查里帮了大忙。压测跑到200并发以上时,就是通过这个断言发现了一批响应虽然状态码是200、错误率为0,但实际返回的奖品信息已经和配置不一致了——这是只看聚合报告根本发现不了的问题。

2.2 阶梯加压的配置过程与参数解读

压测不能一上来就500并发猛压,我采用的是阶梯加压:50并发、100并发、200并发、500并发,每一档持续3分钟,观察各项指标后再升下一档。这样能看到系统在什么量级开始出现性能拐点,而不是直接压垮后拿到一堆无意义的数据。

线程组的配置有几个关键参数值得说说。并发数即线程数,Ramp-Up Period我设置为线程数除以10,意思是50并发时5秒内启动全部线程,500并发时50秒内启动完毕。这个参数不能设得太激进,否则前几秒所有请求同时打到服务器,测出来的响应时间会虚高,不代表真实情况下用户逐渐进入页面的表现。

监听器我加了聚合报告和查看结果树。聚合报告负责出整体指标,结果树负责看单条请求的返回内容。这里提醒一句:正式压测时不要开着结果树跑,它能吃掉大量本地资源,影响压测客户端本身的性能。我实际跑的时候是把结果树关掉的,只看错误日志。

2.3 聚合报告里的关键指标怎么看

聚合报告跑完,先看几个核心数字:样本数、平均响应时间、吞吐量、错误率、P90/P95响应时间。我这里拿到的大致数据是这样:

并发数平均响应时间(ms)TPS错误率P95响应时间(ms)
50864120%145
1001286350%220
2002018920%320
50042311050.16%890

这组数据出现了两个值得深挖的信号。第一个信号是500并发时错误率虽然只有0.16%,看起来不高,但结合Beanshell断言发现了更多逻辑层面的异常响应;第二个信号是P95响应时间从200并发时的320ms跳到了500并发时的890ms,接近三倍的增长,说明系统在500并发已经明显吃紧。

这时候我意识到一个问题:单纯看TPS和响应时间是不够的。庆典现场一旦几百人同时抽奖,奖品库存只有几十件,真正要关注的不是接口响应多快,而是中奖判断准不准。于是我把关注点转向了数据一致性,重新组织了一轮针对奖品库存的压测验证,这也直接引出了后面第四个章节里的关键问题。

3. 用Selenium把庆典流程跑成自动化用例

3.1 用例设计的核心思路:不是点击抽奖那么简单

Selenium自动化这部分,我的目标很明确:把管理员配置奖品、用户扫码进入页面、点击抽奖、查看中奖结果、大屏滚动展示这一整条核心链路自动跑起来,保证每次版本迭代后不用靠人工去点一遍。技术上选用Selenium WebDriver配合Java,测试框架用了TestNG,便于做断言和用例依赖管理。

用例设计上没有简单到"打开页面、点一下按钮、看有没有弹窗"就完事。我拆了8个核心用例,覆盖两层视角:管理员视角的奖品配置和记录导出,用户视角的验证码登录、参与抽奖、结果查看。其中有一个用例专门验证抽奖按钮在点击后的置灰状态——如果按钮没有置灰,就说明前端没有做防重复点击的处理,这是个高风险信号。

自动化用例跑在Chrome上,driver版本必须和浏览器版本匹配,这个老生常谈但每次都会有人踩。我本地Chrome升到新版后,原来的ChromeDriver直接失效,报错信息还特别有迷惑性,提示什么"Chrome failed to start",实际上就是版本号对不上。

3.2 元素定位与显式等待:脚本稳定性的基础

元素定位我优先用id和name,实在没有才用XPath。抽奖页面是前后端分离的,大部分按钮都有明确的id,比如"btn_lucky_draw"、"input_phone"。XPath主要用在大屏展示区域的中奖名单条目定位上,因为那些元素是动态渲染的,类名经常带随机后缀。

真正影响稳定性的还是等待策略。这里我吃过亏,初期用隐式等待,也就是driver.manage().timeouts().implicitlyWait()设置成10秒,跑出来的结果就是脚本时好时坏。后来我系统性排查过:隐式等待在元素存在但不可见、或者页面局部刷新时,经常提前返回,导致后续操作找不到元素。换成显式等待后,通过率明显提升:

WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); WebElement drawButton = wait.until(ExpectedConditions.elementToBeClickable(By.id("btn_lucky_draw"))); drawButton.click();

显式等待的核心是等"条件成立"而不是等"时间过去",elementToBeClickable会同时检查元素存在、可见、可用三点,比单纯等元素出现要可靠得多。

3.3 大屏中奖名单的滚动与可见性验证

大屏展示是抽奖系统的门面,中奖名单会持续滚动刷新,这在自动化里是个特殊的验证点。普通元素直接findElement就能操作,但大屏上不断滚动的列表,元素可能在页面底部,WebDriver默认不会自动滚动到不可见元素上进行某些操作。

我用了JavaScript执行器来强制滚动,先把目标中奖条目滚动到可视区域,再判断其文本内容是否包含预期的中奖人姓名:

JavascriptExecutor js = (JavascriptExecutor) driver; WebElement targetItem = driver.findElement(By.xpath("//div[contains(@class, 'award-list')]/div[contains(text(), '张伟')]")); js.executeScript("arguments[0].scrollIntoView(true);", targetItem); boolean visible = targetItem.isDisplayed();

这个步骤在手工测试时很容易被忽略,但自动化必须显式处理。此外,因为列表是滚动的,我增加了轮询机制,每2秒检查一次目标条目是否出现,最多等15秒。这模拟了用户在大屏上实际"看到"自己名字的过程,也让用例不那么容易受滚动时序影响。

3.4 第一次跑通全套用例的意外收获

第一次跑通8条用例,结果7过1挂:挂掉的是"重复点击抽奖按钮不产生重复中奖记录"这个用例。测试脚本模拟了快速两次点击抽奖按钮,断言中奖记录只有一条。结果系统生成了两条记录。这个发现后来和压测里暴露的幂等问题合并在一起排查了,如果当时只做了手工测试,这种瞬间双击的场景是很可能被漏掉的。

另一件事也值得记录。跑通全套用例耗时大概4分钟,其中大头是等待各种网络请求和动画效果。为了提升执行效率,我在测试环境里把大屏滚动动画的间隔从5秒调到了1秒,动画时长缩短会直接影响用例等待时间,但不影响断言逻辑,这样做是合理的取舍。

4. 压测发现的三个问题,排查链路完整复盘

4.1 高并发下奖品超发:从现象到根因的追溯

这是整个测试过程中最值得说的问题。现象出现在200并发梯度:奖品库存设置为50件,压测结束后去数据库查中奖记录,发现中奖人数是52。多出来2条。

第一步先排除压测脚本的问题。检查了Jmeter的CSV数据源,确认了50个并发用户没有重复手机号,请求参数里的用户标识都是唯一的。

第二步去查数据库日志和应用日志。发现在请求量最大的两秒内,有两条抽奖请求同时读到库存为1,然后同时执行了“库存减1、生成中奖记录”,最终两条请求都判定为"中奖",但库存从1变成了-1。这就定位到了根因:Java后端在库存扣减上用的是典型的"先查询再扣减"逻辑:

// 问题代码逻辑示意 int stock = prizeService.getStock(prizeId); if (stock > 0) { int newStock = stock - 1; prizeService.updateStock(prizeId, newStock); winRecordService.createWinRecord(userId, prizeId); return "中奖"; } return "未中奖";

这个逻辑在单线程下没有问题,但在并发场景下,两个线程同时getStock都拿到1,都判断大于0,都去updateStock,都创建了中奖记录,就超发了。这是典型的读改写竞态条件。

修复方案是开发那边改的,用的是数据库乐观锁配合版本号机制,UPDATE语句加上"WHERE stock > 0 AND version = 上次读取的版本号",同时用AtomicInteger在内存里做一层预扣减。修复后我用同样的压测场景复测,数据验证显示中奖记录数与库存完全一致,这个后面专门讲。

4.2 同一用户重复点击抽奖的幂等缺口

第二个问题其实Selenium自动化已经先发现了,Jmeter压测又给了一次实锤。现象是快速点击两次抽奖按钮,同一个用户生成了两条中奖记录。手工点击时反应没那么快,一般发现不了,但自动化脚本点击间隔可以精确控制在毫秒级,一下就暴露了。

排查下来根因有两个层面。前端层面,抽奖按钮在点击后没有立即置灰,也没有加"请求中"的状态锁,用户在接口返回前可以继续点第二次;后端层面,抽奖接口没有做幂等处理,没有校验"这个用户在当前轮次是否已经抽过奖"。

修复方案是前后端一起改:前端点击后立即disabled按钮,同时显示"抽奖中"的loading状态;后端在抽奖接口里加了幂等校验,同一个用户ID加轮次ID作为唯一键,第二次请求直接返回"已参与本轮抽奖"。

4.3 Selenium脚本偶发找不到元素,问题出在哪

第三个问题属于自动化自身的稳定性问题,但也间接反映了系统前端的一个隐患。压测结束后我重新跑Selenium回归,10次执行里有2次在"点击抽奖后等待结果弹窗"这个环节失败,报错是找不到弹窗元素。

一开始怀疑是元素定位写错了,检查XPath发现没问题。后来通过录制视频回放和一步一步调试发现:点击抽奖按钮后,页面要先发送请求,等待后端返回成功后才弹出结果弹窗。网络正常的时候弹窗很快出现,脚本能找到;网络波动时弹窗延迟到500毫秒以上,脚本在点击后立即去找弹窗元素,自然找不到。

这个问题的本质是异步流程的时序问题。修复方式就是前面讲到的显式等待,把"点击后查询弹窗"改成了"点击后等待弹窗元素可见,最多等10秒"。改完之后连续执行20次,全部通过。这也让我养成一个习惯:凡是涉及异步交互的步骤,一律用显式等待,绝不依赖隐式等待或固定sleep。

5. 修复后的复测验证:性能数据和回归结果对比

5.1 修复方案的验证思路

修复完成后的复测不是简单地把压测再跑一遍,而是要针对修复点做定向验证。我设计了三个维度的验证:

第一,并发准确性能否在高负载下保持。把一等奖、二等奖、三等奖库存分别设为实际数量,跑500并发持续5分钟,结束后逐一核对这些奖品的中奖记录数量是否精确等于库存配置。

第二,幂等逻辑是否生效。用Jmeter模拟同一个手机号在极短时间内连续发送3次抽奖请求,断言结果里只有第一次返回"中奖"或"未中奖"的业务结果,后两次必须返回"重复参与"。

第三,原来的48项功能用例是否受到影响。这里直接跑Selenium的回归用例套件,确认修复没有破坏正常流程。

5.2 复测数据对比与稳定性表现

复测结果对比修复前后,关键指标改善很明显:

指标修复前(500并发)修复后(500并发)变化
平均响应时间423ms356ms-67ms
P95响应时间890ms620ms-270ms
错误率0.16%0%清零
中奖记录精确度多出2条与库存一致修复

这边的响应时间改善其实不是修复并发问题的直接效果,而是开发在改动过程中顺手把查询抽奖结果时的SQL加了索引,顺手优化了一把。但数据一致性这个核心指标的修复是实实在在的:三轮奖品库存分别设置为3件、10件、50件,500并发下跑完,中奖记录分别是3、10、50,一条不多一条不少。

Selenium回归那边,8条核心用例连续执行5轮,共40次执行,只有一次因为测试环境网络闪断导致失败,重试后通过。整体稳定性从最初的"偶发失败"提升到了可以放心作为发布前回归检查的水平。

6. 一点测试心得:庆典抽奖系统到底该怎么测

抽奖助手这个项目做完,我最大的体会是:活动类系统测试的核心不是功能覆盖率,而是风险预判。庆典现场不可能给你留修复时间,所以你要提前把所有"现场可能爆炸"的场景找到并拆掉引信。

具体到工具选型上,Jmeter和Selenium这个组合搭得很顺。Jmeter负责把并发压力打上去,验证系统在流量尖峰下扛不扛得住、数据准不准;Selenium负责把核心流程固化下来,每次改动后能快速回归。前者解决"人多会不会挂",后者解决"改了会不会坏",正好是活动系统最关心的两个问题。

如果你是第一次接手这类系统,有几个建议可以拿走直接用:

  • 压测时一定要把库存数据设置成真实活动的量级,用一万件库存去压50件的活动,压不出超发问题
  • 断言不能只看HTTP状态码和响应体里的success字段,要用Beanshell这种能写逻辑的断言去校验业务字段间的关联
  • Selenium脚本凡是涉及异步加载、弹窗、滚动刷新的环节,务必用显式等待
  • 抽奖这类系统的幂等校验和前端按钮置灰缺一不可,这两道防线少了任何一道,现场都可能出现"一个人中两次奖"的尴尬局面
  • 最后就是回归验证。修复后一定要回到真实场景下复测,而不是只看修复点本身

这个项目之后,我把这套Selenium用例沉淀成了活动类系统的基础回归套件,后来公司办周年庆又组织了一次抽奖,这次没有重新开发,直接复用了这套系统和测试用例,压测跑一遍、回归跑一遍,上线后现场非常顺利。对我来说,这就是测试这件事最有价值的时刻——你提前做的那点工作,让几百人在现场多了一份安心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询