
前阵子接到一个挺有意思的提测需求城郊一座寺庙上线了线上供奉系统香客可以扫码付款、选供奉项目、写功德名后台自动入账。听起来很传统的场景实际上背后跑着一套完整的Web管理系统而我的任务就是围着这套系统写Selenium自动化测试确保香火钱从“香客点击”到“后台入账”整条链路每天都能稳定运行。标题我起得有点夸张“操控寺庙”当然不可能真的操控一座庙我用Selenium操控的是寺庙的线上系统。作为自动化测试工程师我的日常就是和各种业务系统打交道这次的项目让我印象特别深。它不仅业务链路完整、涉及金额校验而且测试环境的坑一个接一个。这篇就把整套自动化测试实践拆开来讲包括业务链路、用例设计、元素定位、对账场景、稳定性优化和最终落地CI的过程。如果你正在用Selenium做Web自动化测试或者准备用pytest组织一套回归体系这篇应该能给你不少能直接抄作业的经验。1. 从“香客扫码”到“入账成功”——寺庙财务系统的三层业务链路1.1 香客看不到的幕后链路绝大多数香客扫码支付之后压根不会想这笔钱是怎么变成寺庙账上的数字的。线上供奉系统其实和电商平台的后台逻辑没有本质区别我梳理了一下整条链路大致分三层第一层是前端供奉页面香客选择供奉类型比如智慧灯、祈福牌、鲜花供养输入功德主姓名填金额然后点击“随喜支付”。第二层是支付网关层用户会被引导到支付二维码或H5收银台这里在测试时不会真的走银行扣款而是通过测试环境的支付桩自动返回“支付成功”的回调。第三层是寺庙后台的入账与对账系统支付回调成功后系统生成订单流水、更新功德主信息、在财务列表里把金额标记为已到账并且提供按日期、按供奉类型、按流水号的对账查询功能。这三层链路里最容易出问题的地方恰恰不是前台交互而是回调解析和入账状态更新。UI自动化测试要验的就是从第一层点下去之后第三层的账对不对。1.2 为什么这里不能只靠手工点点点很多人会想一个寺庙系统的功能又不复杂手工测不就行了刚开始我也这么觉得但真正跑起来才发现问题。这个系统一个月要经历多次迭代今天加一个供奉类型明天改一条支付回调字段后天调一下功德名长度限制每一次变更都意味着回归整条链路。我做了一次统计完整手工走一遍“选项目-填信息-提交-等回调-后台查账-核对金额和时间”熟练的情况下也要五六分钟而且中间只要手一抖点快了就可能重复下单污染测试数据。用Selenium自动化以后同一套流程可以稳定地在几十秒内跑完而且每次执行用的都是同一套测试数据结果更容易对比。这套实践本质上解决了两件事一是把人工回归从重复劳动里解放出来二是把“香火钱入账正确”这个核心业务逻辑变成了一条可以每天自动验证的守护线。2. 测试方案设计——只模拟香客操作不碰真实支付2.1 支付桩测试环境里最关键的约定做这个项目的自动测试之前我第一个要确认的问题就是支付环节怎么处理。绝对不能拿自动化脚本去真实付款一方面是成本问题另一方面是支付环境根本不给你这种权限。我们的方案是测试环境里配置了一套支付Mock服务。前台页面展示的二维码来自一个测试桩地址脚本不去扫码而是由测试桩在收到下单请求后自动回调“支付成功”。Selenium在这里只负责模拟香客的前台操作支付结果由后端测试环境来模拟。这个约定非常重要。如果测试环境没有支付桩UI自动化就只能停在订单生成那一步压根验不到入账结果那这条用例的价值就损失了一大半。所以搭建任何UI自动化框架之前先确认被测系统的外部依赖能不能在测试环境被“打桩”这决定了你的自动化测试到底能覆盖多深的链路。2.2 元素定位策略没有id的页面怎么定位这个寺庙系统的前端历史包袱不轻页面结构大量的table嵌套很多关键按钮没有id或者id是动态生成的。我还遇到过一个奇葩情况同一个按钮在两次页面加载里id后面跟的随机数完全不同。遇到这种页面元素定位策略就要排优先级。我的习惯是优先使用稳定的自定义属性比如开发后来加的>div classoffering-card div classoffering-name智慧灯/div div classoffering-price随喜金额/div /div卡片本身没有id但一个页面上同时存在智慧灯、祈福牌、鲜花供养等多个卡片如果只按offering-card这个class去定位肯定会撞车。我当时的定位方式是# 定位包含指定文字的供奉卡片 donor_type_card driver.find_element( By.XPATH, //div[contains(class,offering-card) and contains(., 智慧灯)] ) donor_type_card.click()这样定位的稳定性比“找第几个class”可靠得多。还有一个细节这个系统没有用任何前端框架的响应式设计布局在不同分辨率下可能错位所以我在自动化脚本里固定了浏览器窗口大小。窗口宽度不一致导致的定位跑偏我后来在多个项目里都遇到过香火钱这个系统也不例外。2.3 测试数据设计每笔善款都要留痕自动化测试里的测试数据不能随便拍脑袋。对账类用例尤其要讲究每笔善款都要能追溯到具体用例要不然两台脚本同时在测试环境里跑你根本分不清某一条入账记录到底是哪条用例产生的。我设计了一套香客数据命名规则测试香客_日期_场景编号比如test_donor_20250121_001。金额数据则覆盖几类典型场景整数金额比如100元验证整数入账没问题。两位小数金额比如188.88元验证金额精度没丢。大额边界值比如9999.99元验证金额位数。最小支付金额比如0.01元这个最容易出问题很多系统在金额格式化时会做异常处理。另外开单前我还会先查一下后台当天已有的流水数作为用例执行的基线。这样断言时就知道新增了几笔而不是只能傻等一个总数。3. 核心用例实战——模拟一次完整的线上“上香”流程3.1 从首页一路点到支付成功整个自动化测试里最基础也最重要的用例就是完整走一遍线上供奉流程。我用pytest来组织用例用Selenium WebDriver驱动浏览器。核心步骤是这样def test_donate_flow(driver): # 打开线上供奉首页 driver.get(base_url) # 选择供奉类型智慧灯 driver.find_element( By.XPATH, //div[contains(class,offering-card) and contains(., 智慧灯)] ).click() # 填写功德名 donor_name_input driver.find_element(By.ID, donorName) donor_name_input.clear() donor_name_input.send_keys(test_donor_20250121_001) # 输入金额 amount_input driver.find_element(By.ID, amountInput) amount_input.clear() amount_input.send_keys(188.88) # 提交订单 driver.find_element(By.ID, submitBtn).click()这里有个大家容易犯的错误send_keys之前没有执行clear()。很多输入框会有默认提示文案比如“请输入功德名”不清空就会导致提交时字段内容不符合预期。我之前吃过这个亏后来把“先clear再send_keys”写成了自己的固定习惯。提交订单之后页面会跳出支付二维码弹窗同时测试环境支付桩会自动回调。脚本在这里等待支付成功提示出现然后断言。3.2 等待策略WebDriverWait比time.sleep好用一百倍新手写自动化时有个通病操作完一个步骤就time.sleep(3)然后继续下一步。这个习惯在寺庙系统里会踩大坑。支付回调是异步的快的时候一两秒慢的时候可能三秒都缓不过来固定sleep太短会失败太长会拖慢整个测试套件。我用的方案是显式等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待支付成功提示出现 WebDriverWait(driver, 15).until( EC.visibility_of_element_located((By.ID, paySuccessTip)) )这个等待是有业务含义的它不是在等一个随机的时间而是在等某个真实的前端状态。支付成功文案没有出现就继续执行说明支付回调可能失败了这时候让用例直接失败反而能暴露系统问题。整个流程中我还对“提交按钮”做了可点击状态的等待WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submitBtn)) )因为金额输入之后前端有一段校验逻辑比如最常见的是校验金额不能过大或不能为负数在校验完成之前提交按钮是置灰的。如果不等它可点击就直接点用例会立刻报错而且会让人误以为是自动化脚本的问题实际上这是前端校验还没准备好。3.3 断言设计不要只断言一个“成功”弹窗支付成功之后页面会出现一个“支付成功”的弹窗很多测试写到这里就结束了觉得用例通过了。这种用例本质上只验证了“前端弹窗出现了”根本不代表账入了系统。我的做法是弹窗出现之后继续往下验证点击“查看我的供奉记录”进入个人供奉列表断言这条订单显示出来并且金额是188.88状态是已到账。关键代码如下# 断言供奉记录列表中出现刚才提交的功德名和金额 record_list driver.find_element(By.CSS_SELECTOR, .donation-records) record_text record_list.text assert test_donor_20250121_001 in record_text assert 188.88 in record_text assert 已到账 in record_text为什么断言要写得这么“重”因为这类系统最常见的bug就是前端提示支付成功但订单状态没有更新或者金额被截断、四舍五入出了误差。只有把断言施加在真实的数据展示上自动化用例才有守护业务的价值而不只是走过场。4. 香火钱对账场景——自动化测试最容易漏掉的一环4.1 对账为什么必须纳入自动化线上供奉系统最核心的财务诉求不是单笔交易成功而是“每一笔都入账、不重不漏”。人工点点点只能验证流程能走通但很难去验证“这笔钱是不是真的进了后台的账”。这里就要做对账自动化。思路很直接在测试环境里制造一笔已知的供奉订单然后去后台系统按流水号查询比对前台展示的金额、时间、供奉类型与后台入账记录是否完全一致。对账对于财务类系统来说是比主流程更重要的场景。我在这个项目里专门建了一个测试目录叫test_audit和主流程用例分开因为它的关注点和用例设计方式完全不同。4.2 设计“一捐二验”的对应用例我把对账用例设计成“一捐二验”第一步是前台捐一笔拿到订单流水号第二步是在后台按流水号搜索第三部是逐一校验订单详情字段。后台搜索的代码如下def get_audit_record_by_order(driver, order_no): driver.get(audit_url) search_input driver.find_element(By.ID, searchInput) search_input.clear() search_input.send_keys(order_no) driver.find_element(By.ID, searchBtn).click() WebDriverWait(driver, 5).until( EC.visibility_of_element_located((By.CSS_SELECTOR, .audit-table tbody tr)) ) records driver.find_elements(By.CSS_SELECTOR, .audit-table tbody tr) assert len(records) 1, f预期查到1条记录实际查到{len(records)}条 return records[0].text这里断言len(records) 1非常重要。有些系统在流水号相同的情况下可能插入了重复数据对账查询如果只取第一行重复入账的问题就永远发现不了。我们在一轮测试中还真抓到了这个bug支付桩重复回调两次系统没有做幂等处理导致同一笔订单生成了两笔入账。如果不对账用例加上“只能有一条记录”的断言这个问题根本测不出来。4.3 金额精度与舍入测试涉及到钱浮点精度是自动化测试里一个绕不开的话题。这个系统在后端计算时用了Java的BigDecimal理论上不会有精度问题但前端展示层进行金额格式化时有可能会出现舍入误差。我的经验是断言阶段尽量用十进制而不是浮点。具体做法是统一从页面文本里提取金额字符串转成Decimal再做比较from decimal import Decimal # 页面展示的金额可能是188.88需要去掉符号 def parse_amount(text): return Decimal(text.replace(, ).strip()) assert parse_amount(record_amount) Decimal(188.88)尽量不要用浮点数比较比如assertEquals(float(188.88), float(188.88))因为浮点数在计算机里不精确比较结果可能因为计算路径不同而出现极其隐蔽的偏差。另外系统还支持“供奉类型叠加”的优惠逻辑。比如选择智慧灯加写祈福牌会有一个组合折扣这时候前端展示金额和后台入账金额就存在一个计算差异。我的建议是在测试数据里把期望金额先用公式算好然后断言后台的入账金额等于期望值而不是断言它等于前端展示值因为前端展示值本身也可能是错的。5. 稳定性优化——把“F5玄学”变成可复现的失败5.1 动态元素与StaleElementReferenceExceptionSelenium自动化跑久了一定会遇到StaleElementReferenceException。这个报错的意思是你定位到了一个元素引用但页面在某个节点被刷新过原来的元素已经脱离DOM你手里的引用变成了“过期引用”。寺庙系统的后台管理页面就有这个毛病查询结果表每次刷新都会重新渲染表格节点。一开始我的脚本里写了这样一段代码rows driver.find_elements(By.CSS_SELECTOR, .audit-table tbody tr) first_row rows[0] # 中间页面刷新了一下 first_row.click() # 这里报StaleElementReferenceException后来我写了一个带重试的点击封装from selenium.common.exceptions import StaleElementReferenceException def click_with_retry(driver, by, value, retry3): for attempt in range(retry): try: element driver.find_element(by, value) element.click() return except StaleElementReferenceException: if attempt retry - 1: raise time.sleep(0.5)这个封装的思路是如果元素过期了就不要在旧引用上死磕重新去DOM里找一次。重试次数一般不用太多三次以内足够因为真正的页面刷新也是瞬间的事情。5.2 浏览器驱动的版本匹配与无头模式Selenium的另一个经典坑是chromedriver版本和Chrome版本不匹配。只要Chrome自动升级了一个大版本而本地chromedriver没更新测试就全部启动失败。这个问题在团队里很容易反复出现我建议在项目README里明确写清版本约束同时准备一个下载驱动的快捷脚本。另外为了让这个套件能挂在服务器上每天跑我最终启用了无头模式from selenium import webdriver options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--window-size1280,960) driver webdriver.Chrome(optionsoptions)有一点要提醒你无头模式下页面渲染行为和真实打开浏览器有细微差异尤其是那些依赖CSS动画的弹层。我在这个项目里发现支付成功弹窗在headless模式下偶尔会晚出现一两秒。这不算bug但会让等待超时。解决方案是把显式等待的超时时间从10秒放宽到15秒而不是去无脑加sleep。5.3 测试隔离每个用例都要用“干净”的香客身份自动化测试最容易出现的假失败是上一个用例产生的脏数据影响下一个用例。这个系统没有登录机制香客身份完全靠浏览器Cookie里的一个会话ID识别如果同一浏览器会话连续跑多个用例个人供奉记录会越攒越多导致断言“只查到一条记录”时失败。我的处理方式是每个测试函数都使用独立的浏览器实例和独立的会话。pytest里这样写fixtureimport pytest from selenium import webdriver pytest.fixture def driver(): options webdriver.ChromeOptions() options.add_argument(--headlessnew) options.add_argument(--window-size1280,960) browser webdriver.Chrome(optionsoptions) yield browser browser.quit()scope不写就是function级别每个用例起一个干净的浏览器环境同时每个用例用的功德名也各不相同从源头上隔离了数据冲突。这是自动化测试稳定性的基础保障。6. pytest组织用例并挂进每日回归6.1 用fixture管理浏览器和测试数据前面的示例里已经使用了pytest fixture但完整的管理方案要更细一点。我需要一个能记录失败的fixture方便出问题时第一时间看到截图。conftest.py里的钩子可以这样写import pytest pytest.hookimpl(hookwrapperTrue) 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 is not None: screenshot_path freports/{item.name}.png driver.save_screenshot(screenshot_path)这样每个用例失败时都会自动留一张现场截图。我强烈建议从项目第一天起就把这个钩子加上因为它能帮你省下大量排查“为什么测试突然红了”的时间。6.2 用目录结构给用例分层我把用例按业务场景分成了三个目录而不是全部堆在一个文件里test_frontend/放主流程用例供奉下单、支付成功、供奉记录展示。test_audit/放对账用例流水查询、金额校验、幂等校验。test_regression/放历史bug回归用例每修一个线上bug就补一个对应的自动化用例进去。这样做的好处特别明显。日常开发提测时我只跑test_frontend快速验证主流程每周做一次全量回归时再跑完整套件。这种分层思路让自动化套件不会变成“跑一次要两个小时但没人关心结果”的僵尸工程。6.3 每天凌晨自动验证香火钱链路最终的落地方式是定时执行。服务器上每天凌晨三点定时跑一次全量回归跑完把报告发送到团队的消息群。这里有一个原则性的提醒UI自动化测试尽量部署在独立的测试环境不要去动生产环境也不要用生产数据。寺庙系统上线后生产环境的数据是真实香客的跑UI自动化极易造成脏数据后果远比测试环境里的失败更严重。我在测试环境定时回归里加了一个简单的冒烟标志每日第一项最先跑“一笔最小金额供奉”确认支付桩、回调、入账链路是通的再跑完整套件。如果冒烟挂了后面的用例大概率全挂就没必要继续浪费时间了。自动化测试最终的价值不是帮你把当前的系统测得多完美而是把核心链路固化成一条可以反复验证的“安全网”。这套香火钱入账系统的Selenium实践本质上和其他行业的Web自动化测试没有区别但它让我更加确信一件事一套好用的UI自动化框架不在于用了多酷炫的技术而在于对业务链路的理解够不够深。你把每一个环节的等待、断言、隔离都考虑到位测试套件自然就稳了。