抽奖系统的测试,最让人心里没底的从来不是某个按钮能不能点,而是那些肉眼看不透的规则到底有没有在线上环境按预期跑。“中奖概率偏差了零点几”、“库存多扣了一次”、“连续快速点击会不会发出两条抽奖请求”,这类问题在演示环境里靠手工点几轮根本说不清。所以我做抽奖系统回归时,习惯把Selenium自动化测试放在关键路径上,用脚本去验证前端交互、结果展示和后端数据变化,把“感觉没问题”变成能落地的结论。
这篇文章会从一个真实的测试需求出发,覆盖抽奖系统测试方案的选择、用例设计、核心代码实现、高频问题排查,以及如何把脚本进化成一套可持续运行的回归工程。适合正在做Web端自动化,或者需要验证抽奖、领奖、库存这类带状态业务场景的同学参考。文章里的代码基于Python 3和Selenium 4编写,即使你平时用Java栈,思路一样能复用。
1. 测试方案定位与前期拆解
1.1 抽奖系统的测试难点到底在哪
抽奖系统表面看只是“点一下按钮,弹一个结果”,实际拆开之后,它比普通CRUD页面麻烦得多,主要难点在四个地方。
第一是概率规则。很多活动页面会根据用户等级、时间段、奖品池配置不同权重,而且前端展示的“转盘”未必对应真实概率。测试时既要验证单次参与返回的奖品是否符合规则,又要在多次抽样之后确认统计概率没有明显漂移。纯手工跑几十次抽奖,费时间不说,还容易数错。
第二是库存一致性。抽奖接口通常会先扣减库存再发奖,有些系统用了Redis lua脚本做原子扣减,有些则是先查剩余数量再更新,存在超发风险。UI层面验证的不只是“中奖后奖品数量变少”,更重要的是并发场景下会不会出现库存变负数、用户已经看到中奖结果但后台没有发奖记录这类问题。
第三是防刷和风控。抽奖活动几乎都会做来源校验、频率限制、登录态校验,有些还会加滑块或验证码。这些逻辑恰恰最容易在自动化执行时爆出来,比如脚本快速点击触发了限流,比如同一个账号短时间请求次数过多被锁定。
第四是前端交互本身。中奖动效、弹窗、抽奖按钮置灰、倒计时刷新、奖品发放到“我的奖品”列表,这类体验细节恰好是接口自动化覆盖不到的盲区。抽奖系统如果不做UI层面的自动化,等于把这些用户真实会感知到的问题全部交给了手工回归,风险很高。
1.2 为什么这次我选择Selenium而不是纯接口测试
很多人一听到抽奖系统,第一反应是写接口自动化。为了验证概率和库存,接口测试确实是效率最高的手段,一个循环每秒能跑十几个请求,比界面快太多。但我在实际项目中并不打算把全部赌注压在接口测试上。
原因很简单:抽奖系统的核心业务是“用户在前台页面感受抽奖”。如果前端因为某个JS报错导致奖品转盘不转,或者接口已经返回中奖但页面没有刷新出结果,接口测试全部通过,用户依然会投诉。再加上现在不少前端框架会把节点动态渲染到DOM里,接口返回的字段在页面上未必一一对应。Selenium这类端到端UI自动化,验证的是从点击到结果展示的完整链路,它补的正是接口测试和最底层单元测试够不着的那一层。
另外,自动化测试的运行频率也不一样。抽奖活动的上线通常伴随运营配置变更,比如奖品池调整、概率从10%改成5%。这类事情不是天天发生,但每次改动都需要快速回归主流程,频率不算高,对运行耗时的容忍度足够,完全适合用Selenium去做冒烟回归。如果是一个每秒千万级请求的秒杀场景,我反而会建议把重心放在接口和压测上,UI自动化只保留最小冒烟集。
1.3 技术栈和测试环境的选定细节
我这次选择的技术栈是Python 3.10 + Selenium 4.x + pytest 7.x + Allure 2。选Python不是因为它一定比Java好,而是因为团队里做测试脚本的人多数会Python,维护成本低,而且Selenium对Python的API支持非常成熟。pytest作为测试框架,参数化能力、fixture机制、插件生态都够用,配合Allure能生成看得懂的HTML报告。
环境层面有几个需要提前确认的细节。
被测系统必须使用独立的测试环境或预发环境,绝对不能对着生产环境跑自动化。抽奖涉及真金白银和用户数据,哪怕一次误操作都可能造成库存被清空或奖品被误发。我一般会在测试环境准备单独的活动ID和奖品池,确保自动化测试的抽奖请求不会影响真实业务数据。
浏览器方面,本地开发时我喜欢用有头模式,方便观察每一步执行。脚本在CI或者定时任务里跑时,改成无头模式。但要注意,无头模式下某些页面交互表现和真实浏览器不完全一样,比如部分浏览器对无头模式下的性能、渲染策略有差异,抽奖动效和弹窗显示时机可能受影响。所以首次搭建脚本时一定要先在有头模式全部跑通,再切无头做稳定性验证。
数据库和Redis的清理权限也很重要。自动化脚本会产生大量抽奖记录、奖品发放记录和订单数据。如果测试环境不能重置数据,反复执行之后统计接口会变得很慢,断言结果也会受到历史数据干扰。我会在每轮完整回归前执行一次数据初始化脚本,把抽奖记录表、用户积分流水、奖品库存全部恢复到基线状态,再把几条测试账号的抽奖次数重置掉。
2. 抽奖场景建模与用例设计逻辑
2.1 核心链路:登录、活动页、抽奖与领奖
抽奖系统的自动化用例设计,我习惯先画主链路,再补充分支。主链路就四条:用户登录、进入活动页、点击抽奖、查看结果与奖品记录。
登录操作比较特殊。抽奖活动往往要求用户必须登录才能参与,有的还要求绑定手机号或完成实名认证。Selenium直接走UI登录最真实,但每次跑脚本都走登录流程会拖慢执行速度,而且遇上验证码就要命。我的做法是分两层:登录流程单独保留一个冒烟用例,走完整UI登录;其他抽奖用例则不重复走UI登录,而是通过调用测试环境的登录接口或构造Cookie,直接把登录态注入浏览器。这样既能保证登录逻辑被覆盖到,又能让主链路脚本跑得更快。
活动页的进入路径也要留意。有些活动是首页广告位跳转到活动页,有些是直接通过URL访问。如果每次都从首页开始点广告位,中间环节太多,定位稳定性差。我会把活动页URL参数化,从直接访问活动页开始跑,只在专门的“活动入口跳转”用例里才验证首页到活动页的完整路径。
抽奖按钮的交互有很多细节要验证。正常状态是“可点击”,点击后按钮不可重复触发,结果弹窗出现后按钮复位。这些状态变化既是我们做断言的依据,也是自动化脚本最容易踩坑的地方。领奖环节通常在中奖后出现,要验证奖品进入“我的奖品”列表、优惠券券码正确展示、实物奖品地址填写流程可以走通。
2.2 概率与库存相关的关键校验点
概率和库存是抽奖系统的灵魂,也是UI自动化最有价值的部分。
概率验证要分成“单次结果正确性”和“多次统计稳定性”两个维度。单次结果正确性,是断言接口返回中奖结果后,页面展示的奖品名称和配置的奖品池一致,比如用户抽中“5元红包”,页面不能出现“10元红包”。多次统计稳定性,通常用固定前置条件跑N次抽奖,统计中奖次数占比,再和期望概率对比。这里不是要做一个统计假设检验,而是设置一个合理区间,比如期望30%中奖率跑60次,允许中奖次数落在11到25之间,超出范围就报警。
注意:概率类断言不能设得太死板。60次抽样本身就有随机波动,如果把断言范围压到“正好18次”,大概率误报。一般我会用二项分布的标准差估算一个阈值,宁可区间放宽一点,也不要在小样本下制造大量不稳定失败。
库存验证的经典做法是把某个奖品库存设置成很小的值,比如只有2份,然后连续抽奖三次以上。断言逻辑是这样的:前面两次中奖后库存依次递减,第三次时页面应返回“奖品已领完”或“库存不足”,不可以再出现成功中奖。更严格一点,要在脚本里同步查询测试库的库存数字,对比界面展示的“剩余数量”是否一致。
并发场景在纯UI自动化里比较难完全复现,但可以模拟出一个简化的“多人同时抽奖”效果。用Selenium Grid同时起多个浏览器实例,每个实例用不同账号在同一活动上抽奖,看库存是否会超发。能跑出几个浏览器已经不错了,真正的并发压力还是要交给JMeter这类工具去压接口,UI层面只验证极端情况下的页面表现。
2.3 异常与反作弊场景用例怎么设计
除了主流程,异常场景设计决定了这套自动化的下限。我总结了几类必须覆盖的边界场景。
未登录状态访问活动页点抽奖,系统应弹出登录引导或跳转登录页,不能出现报错白屏。登录过期之后继续点击抽奖,应被系统提示“登录已过期”,并且不能产生抽奖请求。奖品库存为0时进入活动页,前端应直接置灰抽奖按钮或展示“活动已结束”。重复点击抽奖按钮,页面必须做防重复提交,脚本验证的核心是点击两次之后,后台不会产生两条抽奖记录。中奖弹窗期间再次点击页面其他区域,弹窗不能异常关闭,奖品不能出现重复发放。
反作弊场景自动化要小心处理,不能真的把线上风控系统给触发穿。我的做法是,在测试环境关闭强验证码和滑块,脚本模拟用户常规节奏,随机延时在1到3秒之间,不要用固定间隔疯狂点击。同时验证一个关键点:短时间内同一个账号连续抽奖超过活动上限时,系统应返回“今日抽奖次数已用完”,提示清晰,不影响页面其余功能。
2.4 测试数据的准备和清理策略
做抽奖系统自动化,测试数据是最大的隐形工作量。数据准备不足,脚本跑起来就像隔靴搔痒。
我会把数据准备分成三类。一类是账号数据,至少准备三到五个测试账号,用途不同:一个用于常规抽奖,一个用于库存耗尽场景,一个用于次数超限场景,一个用于未登录场景。另一类是活动配置数据,包括活动ID、奖品池配置、抽奖次数限制、活动上下线时间。第三类是环境状态数据,比如Redis里的奖池剩余数量、发放记录表里的历史数据。
数据准备的方式,优先级最高的是通过后台管理界面或接口批量写入,不要在自动化脚本里依赖手工造数。造数时要把“概率设置”和“库存数量”区分开。比如验证必中场景,把目标奖品概率配成100%,其余奖品概率配成0%;验证必不中场景则反过来。每次用例执行完之后,还必须清理抽奖记录和奖池数据,不然下一轮执行的统计会被污染。
3. 从0到1实现Selenium自动化的关键代码
3.1 基础框架搭建与Driver生命周期管理
搭建框架前,优先确认一件事:项目里有没有统一的WebDriver管理方案。Selenium 4自带的WebDriver Manager可以在CI环境自动匹配浏览器版本和驱动版本,省去手动下载和管理driver的麻烦,这也是我推荐的方案。
基础结构我会拆成三层:conftest.py放fixture和全局钩子,pages目录放页面对象,testcases目录放测试用例。conftest里最关键的是driver的创建和销毁逻辑,保证每个用例执行前拿到一个干净的浏览器会话,执行后及时释放资源。
import pytest from selenium import webdriver from selenium.webdriver.chrome.options import Options @pytest.fixture def driver(): options = Options() options.add_argument("--window-size=1920,1080") options.add_argument("--disable-notifications") # 本地调试时注释掉下面这行,改用有头模式 # options.add_argument("--headless=new") driver = webdriver.Chrome(options=options) driver.set_page_load_timeout(30) yield driver driver.quit()这里有个细节要提醒:driver的实例化如果放在模块级别,浏览器会被所有用例共享,用例之间状态隔离就不是很好,前一个用例遗留的弹窗或Cookie会影响后续用例。所以我坚持用函数级fixture,每个用例一个独立浏览器会话。虽然启动浏览器有性能开销,但稳定性比那几秒的节省更重要。
登录态的注入也是一个fixture要处理的点。通常测试环境会提供一个快速登录接口,脚本先请求接口拿到Cookie,再用driver.add_cookie把登录态写进浏览器,这样所有依赖登录的用例都不需要走一遍UI登录流程。
@pytest.fixture def logged_in_driver(driver): token = fetch_test_token("tester_001") driver.get("https://test.example.com/lottery/summer") driver.add_cookie({"name": "sessionid", "value": token}) driver.refresh() return driver3.2 用Page Object模式封装抽奖页面
Page Object模式听着抽象,实际就是为了让测试用例别直接把CSS选择器写在业务代码里。页面元素位置一变,你只要去改一个页面对应的封装文件,而不是满项目到处找选择器。
抽奖页面我通常抽象成三个对象:登录页、活动页、奖品记录页。活动页是最核心的,它至少包含抽奖按钮、活动标题、剩余抽奖次数、中奖结果弹窗这些元素。点击抽奖是一个动作,获取结果是一个动作,关闭弹窗又是一个动作。
from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LotteryPage: TITLE = "某活动抽奖" def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def open_activity(self, activity_id): self.driver.get(f"https://test.example.com/lottery/{activity_id}") def click_lottery(self): btn = self.wait.until( EC.element_to_be_clickable((By.ID, "lotteryBtn")) ) btn.click() def get_result_text(self): result = self.wait.until( EC.visibility_of_element_located((By.CSS_SELECTOR, ".lottery-result")) ) return result.text.strip() def close_result_popup(self): close_btn = self.wait.until( EC.element_to_be_clickable((By.CSS_SELECTOR, ".popup-close")) ) close_btn.click()封装类里不写assert,是Page Object模式的铁律。页面对外只暴露操作和状态获取,具体断言全部放在测试用例层。比如用例里可以断言返回文本是“恭喜”,但页面类本身不关心结果对错,这样职责才清晰。
实际封装时,我还会把“抽一次奖并拿到结果”合并成一个组合方法。组合方法的好处是,概率验证用例里只需要循环调用这一个方法,不必每次都手动处理弹窗。
def draw_once(self): self.click_lottery() result = self.get_result_text() self.close_result_popup() return result3.3 元素定位与等待策略,自动化稳定性的生命线
抽奖系统的前端通常大量使用Vue或React,元素是动态渲染的,class名经常带hash后缀,比如lottery-btn__2jk3a。这类动态class一旦前端发版就会变,定位方式如果只依赖class,脚本很快报废。
我更推荐优先用稳定的业务属性,比如id、name、aria-label,或者通过XPath定位“包含某个文本的按钮”。实在没有稳定属性时,再用相对定位,比如找某个容器的后代节点。还有一点是,能不用XPath就不用XPath,CSS选择器在绝大多数场景下性能更好。
等待策略是所有Selenium脚本的命脉。很多同学习惯在代码里写time.sleep(3),遇到慢页面就改成sleep(5),这种写法是对稳定性最大的伤害。服务器偶尔慢了一秒,固定等待时间就失效;服务器提前完成,又白白浪费等待时间。正确做法是用显式等待,让脚本等到指定条件满足立即往下走,超时再报错。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC btn = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "lotteryBtn")) )抽奖场景里最常见的等待坑是动效还没结束。点击抽奖按钮后,前端会先播放转盘动画,再弹出结果。如果脚本在动画期间就去查找结果元素,元素可能还没挂载到DOM上。针对这种情况,不要只等元素存在,要等它可见且文本非空。也可以用text_to_be_present_in_element这类条件,明确等待特定文案出现。
如果页面里有iframe嵌套,比如抽奖模块被嵌在活动主页面里的子页面中,必须先把driver切换到对应iframe才能定位内部元素。这个我在项目里踩过不少坑,后面问题排查部分会展开。
3.4 数据驱动的参数化用例与断言细节
抽奖用例最大的特点就是重复性高:同样的抽奖动作,要验证不同奖品配置、不同账号类型、不同抽奖次数。把这些重复抽出来,用pytest的参数化一次搞定,是提升维护效率的关键。
参数化有三个维度最常用。第一个是奖品池类型,比如“普通奖品池”、“高端奖品池”、“空奖品池”。第二个是用户类型,比如“新用户”、“老用户”、“已抽完次数用户”。第三个是动作组合,比如“抽一次”、“连抽三次”、“抽奖中刷新页面”。把这些维度组合起来,写成数据驱动用例,维护起来非常舒服。
import pytest @pytest.mark.parametrize( "activity_id, expected_prefix", [ ("lottery_normal", "恭喜"), ("lottery_hightier", "幸运"), ("lottery_empty", "已领完"), ] ) def test_lottery_result(logged_in_driver, activity_id, expected_prefix): page = LotteryPage(logged_in_driver) page.open_activity(activity_id) result = page.draw_once() assert expected_prefix in result断言方面,接口返回和页面展示的一致性是我单独加的检查点,不只是检查文案。比如抽奖结果弹窗里展示的红包金额,我需要拿它和接口返回的实际金额做个对比。这个做起来不复杂,但能抓住不少前端取值错误的问题。
def test_red_packet_amount_matches_interface(logged_in_driver): page = LotteryPage(logged_in_driver) page.open_activity("lottery_redpacket") result_text = page.draw_once() actual_amount = float(page.get_display_amount()) expected_amount = page.get_api_amount_for_current_result() assert actual_amount == expected_amount注意,这段代码里的get_api_amount_for_current_result在真实框架里往往是通过浏览器导入Har或拦截请求来获取,但在简单场景下可以改成读取接口返回存到页面上。实际实现方式取决于你的项目架构,核心思路是“页面显示值和后端返回值要相互印证”。
4. 真实执行中的高频问题与排查实录
4.1 元素找不到、点击无反应怎么查
运行Selenium脚本时,报错最多的就是NoSuchElementException和ElementClickInterceptedException。这些错误看起来是同一个原因——“元素定位失败”,实际背后可能有多种情况。
最常见的三种场景。第一,元素真的不在当前页面,可能是弹窗遮住了页面主体、tab切换导致DOM重绘、iframe没有切换。第二,元素在DOM里存在,但不是“可点击”状态,按钮被置灰、被loading遮罩挡住、被fixed定位的悬浮层覆盖,这时click()会报元素不可点击。第三,元素定位器本身过期了,前端发版后id或class发生了变化。
排查时我会按固定顺序来:先在浏览器控制台手动执行document.querySelector,确认页面里到底有没有这个元素;再检查是否存在iframe嵌套,在DevTools里看元素是否在frame里;再检查元素在页面上的可见区域,看看有没有遮罩层挡着;最后再看元素是否被动态替换。定位符写得太年轻也是老问题,尽量用稳定的属性,而非动态生成的文本或class。
有些时候确实是业务逻辑导致页面没有渲染出该出现的元素,这种“假失败”背后往往藏着真bug。比如库存不足时按钮直接不显示,而不是显示“已领完”,这可能是前端没做状态分支,属于产品缺陷,不是脚本问题。
4.2 抽奖动效和弹窗导致断言失败
抽奖页面非常喜欢用CSS动画和延迟弹窗,比如转盘转3秒才出结果、中奖弹窗带渐入效果。这类动效会让自动化脚本的时序变得很不确定。
我遇到过一个真实案例:点击抽奖后,脚本用sleep(2)等结果,本机跑得好好的,换到一台性能较差的CI机器上,2秒根本没播完动画,结果元素还没出现,用例直接失败。这就是固定等待的标准翻车现场。
后来我把所有跟动画相关的等待都改成显式等待,同时结合轮询和重试机制。有一个技巧是“等待元素状态稳定”:先等结果元素出现,再等它的文本值不再变化,这样能保证动画结束、文案刷新完成后再做断言。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC result = WebDriverWait(driver, 10).until( lambda d: d.find_element(By.CSS_SELECTOR, ".lottery-result") ) WebDriverWait(driver, 10).until( lambda d: result.text )如果弹窗本身有模糊、渐入、渐出效果,不要直接点击关闭按钮,容易点在过渡期间导致事件丢失。加一步前置等待,先断言弹窗右上角的关闭按钮可点击,再执行点击。
另外,断言文案本身也可能是坑。中奖弹窗会先显示一句“正在开奖...”,再变成“恭喜获得5元红包”。如果脚本在过渡文案还没刷新时就断言,结果自然是失败的。这种场景最佳做法是断言最终文案,而不是中间状态。
4.3 并行执行时窗口资源互相干扰
跑Selenium自动化最爽的是“并行”,最痛的也是“并行”。我一开始天真地把用例全部交给pytest-xdist并行执行,结果跑着跑着就出一大堆莫名其妙的失败:页面元素找不到、登录Cookie串号、浏览器资源不足导致页面白屏。
并行资源干扰通常出现在两个层面。操作系统层面,一台机器同时起十几个浏览器,CPU和内存很容易被吃满,尤其是无头模式下的Chromium占内存很离谱。业务逻辑层面,多个浏览器实例共用一个测试账号的时候,A实例和B实例同时在同一个活动页抽奖,频率限制就会立刻触发,抽奖接口返回异常,页面展示自然对不上。
我的解决思路是分层控流。第一,给测试账号做隔离,每个并行worker使用独立账号。第二,限制并行度,单机并行数控制在4以内,再多就交给Selenium Grid分布式跑。第三,用例设计上尽量避免多个并行用例共享同一个秒级敏感的活动配置,比如库存只剩1份的活动就不要让一堆用例同时去抽。
如果在脚本里还是需要短暂停顿,比如等待动画或等待接口返回,我一般用随机延时替代固定延时,让每个并行worker的节奏错开,降低同时发请求的概率。不要小看这个细节,它经常能减少一半以上的偶发失败。
4.4 服务端风控和限流带来的假性失败
抽奖系统的服务端几乎都会有风控策略,测试环境也不例外。自动化脚本一旦操作节奏太快,比如连续几秒内频繁点击,风控就可能把当前账号临时限制住,接口开始返回错误码。
从脚本视角看,前面一次用例正常运行,下一次用例一进来账号就被提示“操作太频繁”,所有断言全军覆没。这种失败和脚本本身没关系,纯粹是业务策略触发,处理不好会让我们陷入无限改脚本、加了等待又触发超时的新怪圈。
我的应对策略分三层。第一层,测试环境配置上,把风控阈值调高,或者干脆关闭验证码、滑块这类强交互验证,把风控从“阻断”降级为“日志记录”。第二层,脚本层面控制速率,每个用户操作之间加随机延时,确保单账号单位时间请求次数不超过正常用户上限。第三层,用例层面识别风控拦截特征,一旦页面出现“操作频繁”的提示,主动跳过当前用例并明确标记为“风控拦截”,而不是让测试框报一个元素超时的迷惑错误。
这里要特别说一句:风控逻辑也是系统功能的一部分,完全绕过它去测试是不对的。正确的做法是保留一条独立的“风控专项用例”,单独验证限流规则和拦截提示是否正常,而让业务主流程用例在相对宽松的环境下稳定运行。把这两件事混在一起,业务用例会变得极其脆弱。
5. 抽奖回归自动化的工程化进阶
5.1 用Selenium Grid把执行时间压下来
抽奖用例和数据驱动组合一旦多起来,串行执行能跑到半小时以上。这个耗时在定时巡检里勉强能忍,但要做发布前的冒烟回归就太慢了。我的解决方案是上Selenium Grid,让用例分散到多台机器的浏览器节点上跑。
Grid的部署方式,最轻量的是在Docker里起一个hub节点和几个node节点,每个node注册一个或多个浏览器槽位。测试代码不再直接连本地的WebDriver,而是通过RemoteWebDriver连接hub的地址。
from selenium import webdriver from selenium.webdriver.common.options import ArgOptions options = webdriver.ChromeOptions() driver = webdriver.Remote( command_executor="http://127.0.0.1:4444/wd/hub", options=options )命令执行器和options之间还涉及到一些版本参数的兼容性,比如浏览器版本号要固定,node节点不能用“最新版”策略,不然某次镜像更新后所有用例一起挂。这些参数配置在Grid的toml配置文件里就处理掉了,远比在代码里指定版本号灵活。
部署完Grid之后,再配pytest-xdist分布式并行,让不同worker从hub申请不同浏览器槽位,整体执行时间能缩短到原来的四分之一。当然,缩短时间的前提是用例之间数据隔离做得好,否则并行加速只会换来满屏的偶发失败。
5.2 失败现场:截图、日志与录屏的组合证据链
自动化跑到凌晨报错了,等第二天看到一条“元素找不到”的文本日志,你根本不知道线上发生了什么。所以我在工程化阶段坚持给每次失败保留三件套:截图、页面源码、带执行步骤的日志。
截图最好在失败钩子里统一完成,而不是散落在每个用例里。用pytest的钩子拿到失败报告后,把当前driver的截图保存下来,同时把driver.page_source存成HTML文件,这样不仅能看当时画面,还能查当时DOM结构。
import pytest import time @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.funcargs.get("driver") if driver: timestamp = int(time.time()) driver.save_screenshot(f"reports/{item.name}_{timestamp}.png") with open(f"reports/{item.name}_{timestamp}.html", "w", encoding="utf-8") as f: f.write(driver.page_source)比截图更进一步的是录屏。Selenium本身不提供录屏能力,但可以用浏览器DevTools Protocol里开启录屏,或者通过外部命令录制整个浏览器窗口。录屏文件比较大,我通常只在排查疑难偶发问题时临时打开,平时默认只开截图和源码。另外,日志里要记录当前用例操作到哪一步,最好是在每个Page Object方法里打点记录,失败时能直接定位到“点击抽奖按钮成功,等待结果超时”这样的细节,而不是只看一个空泛的异常堆栈。
5.3 在CI流水线里做定时巡检和发布前回归
不接CI的自动化脚本,说难听点就是一次性工具。我在团队里把抽奖自动化分成了两层跑法。
第一层是定时巡检。每天凌晨在测试环境跑一套精简冒烟集,覆盖主链路、库存耗尽、次数超限和最核心的概率区间校验。巡检任务是低频的,晚上跑对白天开发的干扰也小。一旦早上发现失败,团队有时间在业务上线前定位问题。第二层是发布前回归。当有新的抽奖活动上线时,在流水线的发布前阶段完整跑一遍抽奖用例集,包含所有数据驱动组合。发布前回归要求快速反馈,所以这层一定接上Selenium Grid并行执行,执行时间尽量控制在10分钟以内。
CI里的集成方式,不管是Jenkins还是GitLab CI,核心逻辑都是拉取测试代码、安装依赖、执行pytest、收集Allure报告、发送通知。流水线里还要固定浏览器镜像和依赖版本,防止今天跑通过明天就跑挂。
test-lottery: stage: test script: - pip install -r requirements.txt - pytest testcases --maxfail=3 -n 4 --alluredir=allure-results artifacts: paths: - allure-results/ - reports/ expire_in: 30 days only: - schedules - tags这里的调度触发和产物收集要看团队实际用的CI系统,但核心思路不变:以产物形式把测试报告留存下来,方便事后追溯。Allure报告里的用例历史趋势特别有用,能清晰看到某条用例过去三周是不是反复在挂,这种“稳定地不稳定”的用例是后续维护的重点对象。
我个人在实际操作中最大的体会是:抽奖系统的自动化测试,脚本和页面代码写出来只是第一步,真正的价值在于用例设计和数据隔离,以及把执行结果接入日常的巡检反馈。不要指望一套脚本写完就一劳永逸,抽奖活动的奖品配置在变、页面动效在变、风控策略也在变,自动化脚本和这些变化是需要长期磨合的。每次失败都可能是产品改动或环境变动发出的信号,把信号的排查过程记下来,这套自动化系统才会越来越稳。最后再分享一个小技巧:给抽奖系统的每一个页面对象方法都加上执行日志,并且把关键接口请求记录下来,排查时会省掉非常多的时间。