1. 客户项目的第一道坎:环境准备比写脚本更耗时
之前接过一个客户端的自动化测试项目,需求说得很简单:“把我们的核心业务链路用 Selenium 自动化跑起来,以后回归不用人工点。”一开始我以为是技术问题,结果真正开工后才发现,最耗时间的根本不是定位元素、写用例、调等待,而是把环境理清楚。
客户现场的环境跟咱们自己电脑上完全不一样。你本地用 Chrome 120 + Selenium 4.15 跑得好好的,客户那边的办公电脑可能还在用 Chrome 98,甚至浏览器是 IE 模式下的 Edge。更要命的是,客户的 IT 策略经常会锁浏览器自动更新,一个大版本升上去,旧版 chromedriver 立刻失效,整个套件直接崩掉。
我自己在客户项目里的第一个经验就是:不要把环境这件事想得太简单,也不要把“我本地能跑”当成“客户那边能跑”。接手 Selenium 客户项目的第一步,先做一份环境摸底清单,挨个确认下面这些东西:
- 客户指定的浏览器类型和版本(不是“能用就行”,要精确到大版本)
- WebDriver(chromedriver / geckodriver / edgedriver)是否已经放好,版本是否和浏览器匹配
- 客户机器是否有外网权限,Selenium 脚本里如果引用了 CDN 资源、远程加载的字体、第三方统计脚本,会不会因为连不上外网导致页面一直转圈
- 屏幕分辨率、缩放比例(Windows 显示缩放如果设成 125% 或 150%,页面元素的位置会发生很大变化,非常影响截图和点击坐标)
- 是否有杀毒软件、安全助手在拦截浏览器进程,导致 Selenium 启动失败或者中途崩溃
一个必须养成的习惯是:客户现场的浏览器和驱动版本要写进交付文档,并且用配置文件统一管理。不要把事情寄托在“客户的电脑应该是最新版”这种幻觉上,也别指望客户自己会去更新。
我在这个项目里用一个简单的config.yaml管理驱动路径和浏览器版本,脚本启动时自动做一次版本检查:
browser: chrome executable_path: drivers/chromedriver.exe window_size: 1920,1080from selenium import webdriver from selenium.webdriver.chrome.service import Service import yaml, subprocess with open("config.yaml", "r", encoding="utf-8") as f: cfg = yaml.safe_load(f) # 检查浏览器主版本号 chrome_ver = subprocess.check_output( ["reg", "query", r"HKEY_CURRENT_USER\Software\Google\Chrome\BLBeacon", "/v", "version"], shell=True ).decode("utf-8") major = chrome_ver.strip().split(".")[0] print("当前 Chrome 主版本:", major) service = Service(cfg["executable_path"]) driver = webdriver.Chrome(service=service) driver.set_window_size(cfg["window_size"]["width"], cfg["window_size"]["height"])驱动和浏览器版本的对应关系是 Selenium 项目最经典的坑,没有之一。这里直接放一个经验值表格,按常见大版本对应就好:
| 浏览器版本(Chrome) | 推荐 chromedriver 版本 | 备注 |
|---|---|---|
| Chrome 114 | 114.0.5735.90 | 旧项目常用,注意锁定 |
| Chrome 116 | 116.0.5845.96 | 部分客户环境停留版本 |
| Chrome 120 | 120.0.6099.109 | 稳定,兼容性好 |
| Chrome 126 | 126.0.6478.126 | 需要新驱动,旧脚本可能不兼容 |
| Chrome 130+ | 请用 130 以上的对应驱动 | Selenium 4.20+ 配合使用更稳 |
装驱动的时候,检查的不只是“驱动能不能启动”,重点是驱动和浏览器主版本是否一致。Selenium 在驱动版本不匹配的时候并不会给你一个友好的报错,往往就是session not created或者一个诡异的connection refused,排查起来非常浪费时间。
1.1 客户机器上最容易踩的 Selenium 启动底层问题
除了版本匹配,客户机器上还有两个高频问题:端口被占用和加载项权限。
Selenium 启动浏览器时会自动占用一个随机端口来做 DevTools 通信。如果客户机器上之前有 Java 服务、数据库管理工具占用了这个端口区间,浏览器是能弹出来,但后续所有脚本指令都会卡死。我遇到过一次非常诡异的现象:脚本启动浏览器正常,页面打开正常,但只要一执行find_element,就报Failed to establish a new connection,最后发现是客户机器上某个安全监控软件把 localhost 的随机端口全部拦截了。
这种情况的规避方式有两个思路:一是显式指定端口号,避免随机端口冲突;二是跟客户沟通把脚本要用到的端口段加白。经验做法是固定用一个端口范围,减少随机性:
from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--remote-debugging-port=9222") driver = webdriver.Chrome(options=options)还有 Windows 下最常见的:客户机器上有 360、火绒或者其他安全软件开着“弹窗拦截”和“网页防护”,Selenium 启动的 Chrome 会被误判为没有界面的后台进程,导致页面加载极慢,甚至停在空白页不加载。这个不用硬刚,跟客户 IT 说明情况,把 Selenium 启动的浏览器进程加入白名单,或者让客户临时关闭防护软件做验证。注意,这不是技术能不能解决的问题,是人跟人的沟通问题,很多同学在这个环节吃亏是因为不敢找客户确认。
1.2 一套可直接复制到客户机器的环境检查脚本
为了让环境问题不再反复出现在项目中期,我把环境检查做成了一个独立的小脚本,复制到客户机器上运行一次,就能把关键信息全部打出来。这个脚本的成本很低,收益却很高,可以作为 Selenium 项目的“环境体检”标准动作。
import platform import subprocess import socket def check_os(): print("操作系统:", platform.system(), platform.release()) def check_chrome(): try: out = subprocess.check_output( ["reg", "query", r"HKEY_CURRENT_USER\Software\Google\Chrome\BLBeacon", "/v", "version"], shell=True ).decode("utf-8") print("Chrome 版本:", out.strip().split(" ")[-1]) except Exception as e: print("未检测到 Chrome:", e) def check_port(port): s = socket.socket() try: s.bind(("127.0.0.1", port)) print(f"端口 {port}: 可用") except OSError: print(f"端口 {port}: 被占用") finally: s.close() def main(): check_os() check_chrome() for p in [9222, 9515, 4444]: check_port(p) if __name__ == "__main__": main()这个脚本不是给我自己用的,是给客户现场执行的,所以输出必须清晰直白,不能让客户去猜“这个字段是什么意思”。客户只要把输出结果发回给你,你远程就能判断环境有没有问题,省了一趟又一趟的现场排查。
2. 元素定位策略:客户系统的真实页面远比 Demo 复杂
环境跑通之后,真正的考验才刚开始:写 Selenium 用例。这一步新手最容易掉进去的陷阱是照着自己搭的练手页面来写定位,觉得“用 id 定位很简单嘛”,结果一放到客户系统里全是问题。客户的核心业务系统往往经历了多年迭代,前端用的是老掉牙的框架,页面结构混乱,class 名称重复率极高,id 更是随心所欲——同一个按钮,两个不同页面里竟然有同样的 id,这在老系统里很常见。
我在客户项目里积累的一套元素定位优先级是这样的:
- 优先使用 id:前提是页面里唯一,且不会动态变化
- 其次使用 name:表单类元素常出现,但要确认不是动态拼接的
- CSS 选择器:适合相对路径定位、层级定位,配合类名和属性
- XPath 绝对路径:狠活,除非不得已不要用,后面会说为什么
- 文本定位(XPath text 或 partial link text):适合按钮、链接等有明确显示文本的元素
不要一开始就无脑用 XPath 里的//*[contains(@class, 'xxx')],尤其不要用浏览器“复制 XPath”功能生成的完整绝对路径。浏览器自动复制出来的 XPath 往往是这样的:
/html/body/div[3]/div/div[2]/div[1]/div/div[2]/div[2]/div[2]/table/tbody/tr[3]/td[2]/button这种定位方式看起来能用,但任何一处结构变动都会把它打死,页面上多一行数据、加一个弹窗,它就找不到了。客户项目的回归测试本来就是为了应对系统频繁改动,结果你写了个比系统还脆弱的定位,等于给自己埋定时炸弹。
2.1 iframe 与多级弹窗:客户系统里躲不掉的坑
老系统特别喜欢用 iframe,尤其是登录后的一些报表页面、附加上传页面,内容全在 iframe 里。很多人第一次遇到这种情况会特别困惑:明明元素就在页面上,打开 F12 也看得到,可 Selenium 就是定位不到。
这不是你眼睛出问题了,是元素在当前的主文档 DOM 里根本不存在,它在 iframe 这个独立文档里。Selenium 默认只操作主文档,要操作 iframe 里的元素,必须先切换到 iframe 上下文里。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 等待 iframe 可切换并进入 iframe = WebDriverWait(driver, 10).until( EC.frame_to_be_available_and_switch_to_it((By.ID, "mainIframe")) ) # 之后定位的元素就是 iframe 内的 driver.find_element(By.ID, "uploadBtn").click() # 操作完必须切回主文档,否则后面主页面元素全部找不到 driver.switch_to.default_content()类似的问题还有嵌套 iframe。如果系统里有一个 iframe 里套另一个 iframe,你得一层层切进去,操作完再一层层退回来。很多人只记得最后切回主文档,中间忘了一层,就会导致后续定位全部崩掉。建议在每个用例结束后统一做driver.switch_to.default_content(),把上下文状态恢复干净,避免用例之间相互污染。
2.2 动态属性与重复 class:换个姿势定位
老系统里还有一种常见场景:元素的 id 和 name 是后端动态生成的,每次刷新页面都会变,类似id="ctl00_ContentPlaceHolder1_txtName_12345",后面那串数字每次都不一样。这时候你要是敢把 id 写死在用例里,跑第二次就跪。
处理方式有两个思路:
- 用稳定的父级容器做锚点,再往下找子元素。比如先定位
div#searchPanel,再找它下面的input[type='text'] - 用属性包含匹配,
[id*='txtName']这种模糊匹配来处理动态变化的后缀
再有一个坑:页面里 class 名高度重复。客户系统用了比较老的 UI 框架,按钮的 class 基本都是btn btn-default,页面里有几十个一样的按钮。你光靠 class 是分不清哪个是“保存”、哪个是“取消”的。这时候就要用相对路径+文本结合:
# 根据可见文本定位按钮 save_btn = driver.find_element( By.XPATH, "//div[@class='btn-group']//button[contains(text(), '保存')]" )文本定位也有讲究,contains(text(), '保存')比text()='保存'更稳妥,因为按钮里经常有前后空格或者换行符。而且尽量把范围缩小到某个容器内再找文本,不要全局//button[contains(text(), '保存')],万一页面底部弹出一个“保存成功”提示框也匹配上,就不妙了。
2.3 减少对前端源码依赖的定位哲学
做多了客户项目之后,我的体会是:元素定位写得越稳定,后期维护的成本越低。所谓稳定,不是“今天能跑通”,而是“前端改了几次还能少动就少动”。基于这个原则,我总结了几条实战经验:
- 封面父容器用有业务含义的 id,不要依赖层级太深的路径
- XPath 优先用相对逻辑定位,比如
//li[contains(@class, 'active')]/a,而不是//html/body/... - 能不用索引 index ['tr[3]'] 就不用,因为列表排序一变,索引就全废
- 一个用例只有一个主入口定位,如果定位失败,不要写一堆 try 去猜,而是快速失败并截图,问题越早暴露越好
3. 等待策略:客户现场的“慢”比想象中更致命
Selenium 里最常见、最让人抓狂的问题就是元素定位不到。十次里有八次,不是定位表达式写错了,而是元素还没加载出来。主要体现在三种情况:
- 页面打开后,浏览器地址栏已经加载完,但 JS 还没执行完,按钮还在禁用态
- 页面某个区域是异步请求渲染的,后端接口慢,前端一直转圈
- 页面里引了监控脚本、统计脚本,阻塞了后续 DOM 渲染
新手最原始的做法是time.sleep(5),这里睡三秒,那里睡五秒。短了不够,长了浪费时间,整个脚本跑下来一半时间都在发呆。更重要的是,sleep 是死等,哪怕元素一秒后就已经出现了,它也非要等满 5 秒才继续,执行效率低到让人崩溃。
我见过跑一个客户回归套件需要 3 小时半的,里面有 200 多个time.sleep,后来我把它们全部换成显式等待,套件总时长直接砍到 40 分钟。这就是等待策略的差距。
3.1 三种等待方式的本质区别
Selenium 里显式的等待思路是这样的:主动询问“你要找的元素出来了吗”,如果没出来,每隔一段时间再问一次,直到超过设定的超时时间才报错。这种方式最可靠,因为它的判断条件是“我想要的那个元素已经就位”,而不是“我感觉页面应该加载完了”。
下面用一个表格来对照三种等待方式,方便一眼看清区别:
| 等待方式 | 实现方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 强制等待 | time.sleep(n) | 简单粗暴 | 固定时长,效率极低 | 几乎不用,只用于调试 |
| 隐式等待 | driver.implicitly_wait(n) | 设置一次全局生效 | 可能掩盖真正的问题;对兄弟元素也生效,容易假通过 | 配合显式等待做兜底 |
| 显式等待 | WebDriverWait + expected_conditions | 精确控制等待条件,效率最高 | 每个关键元素都要写一段代码 | 核心业务元素、异步加载元素 |
隐式等待有个特别坑的地方是:它不只是等待你下一个定位操作,而是对整个 driver 实例所有查找操作生效。也就是说,有些元素明明还不存在、应该让用例报错来告诉你页面有问题,但因为隐式等待给它“加了 10 秒缓冲”,它愣是等到了元素被渲染出来。这样反而掩盖了页面性能退化的真实情况。所以我现在的用法是:全局隐式等待设一个较小的值(比如 3 秒)作为兜底,关键操作全部用显式等待精确控制。
3.2 显式等待的正确写法与常见误用
显式等待的核心 API 是WebDriverWait配合expected_conditions。很多教程只让你用到element_to_be_clickable,但实际客户项目里,不同场景要用不同的条件,写错了照样不稳定。
我整理了几个常用场景的等待条件:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait = WebDriverWait(driver, 15) # 场景1:页面跳转后,等待某个关键元素出现在 DOM 中即可 wait.until(EC.presence_of_element_located((By.ID, "orderList"))) # 场景2:点击按钮前,必须等按钮可点击(可见且未禁用) wait.until(EC.element_to_be_clickable((By.ID, "submitBtn"))) # 场景3:等待某个元素从页面上消失,比如加载遮罩层退去 wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, "loading-mask"))) # 场景4:等待浏览器标题变成预期值,常用于判断页面跳转完成 wait.until(EC.title_contains("订单详情"))很多人会在“元素可见就点击”这件事上栽跟头。presence_of_element_located表示元素已经在 DOM 里了,但它可能还在屏幕外、可能被遮罩层挡住、可能处于 disabled 状态,这时直接click()会报ElementClickInterceptedException或者明明点了但是没反应。所以点击前一定要用element_to_be_clickable,而不是presence_of_element_located。
3.3 页面加载姿态与超时阈值:一个可复用的设计
在客户项目里,不同操作等待的时间阈值完全不一样。登录页可能 3 秒就出来了,但列表查询可能要等接口 10 秒,导出报表甚至要等 30 秒。用一个统一的超时时间是不现实的。
我建议把一个用例里的等待阈值单独抽成一个配置项,按操作类型区分:
| 操作类型 | 建议超时时间 | 说明 |
|---|---|---|
| 页面跳转 | 10~15 秒 | 超过这个时间多半是网络或性能问题 |
| 按钮点击 | 10~15 秒 | 点击后要确认下一步操作生效 |
| 列表查询 | 15~30 秒 | 老系统的报表查询接口比较慢 |
| 文件导出 | 60 秒以上 | 涉及后台处理,不只看页面,还要检查文件是否生成 |
| 大规模数据加载 | 60 秒以上 | 上万条数据的表格渲染极慢 |
超时时间不是越大越好。设置过大的等待会让失败的用例拖很久才报错,整个套件的执行时间被无意义地拉长。我的建议是:先按经验值设置,跑几天之后看日志,把经常在临界点附近超时的元素单独调大,而不是一刀切全部改大。
等待策略还有一个很容易忽略的细节:页面加载完成后,不是立刻能操作。浏览器地址栏加载完成和页面交互元素可用是两码事,尤其是一些老系统,DOM 加载完后还要跑好几段 jQuery 去绑定事件,这个时候元素已经存在了,但是点击没有绑定事件,click()不会报错,但功能没触发。这种情况下,最稳妥的判断条件是“点击后预期的变化已经出现”,比如点击保存后等待“保存成功”提示、等待列表刷新出现新数据,这才是真正在验证业务结果,而不是只验证“元素可以被点击”。
4. 数据与环境状态:客户项目的隐形雷区
很多人写 Selenium 用例时只盯着页面元素,忘了问一个问题:这个用例跑的时候,系统里有没有它需要的数据?客户环境不等于测试环境,客户的核心系统里跑的都是真实数据,随便造数、删数都可能出大事。
我在客户项目里踩过的数据坑包括:用例需要查一个“待审核”的单子,但客户系统里恰好没有“待审核”状态的单子,脚本直接失败;共享的测试账号之前被同事跑用例改坏了某个配置项,之后几天所有用例全部失败;往正式环境灌了一大堆测试数据,结果客户那边真实客户看到了脏数据。
面对这类问题,必须养成“前置数据准备”的习惯:用例执行之前,先把前置条件准备好;用例执行之后,最好把数据恢复到初始状态。Selenium 里做数据准备通常有三种方式:
- 通过页面操作准备数据:最真实,但速度慢,还容易受页面改版影响
- 通过数据库直接插入/清理数据:可靠高效,但需要数据库权限和备份意识
- 通过后端接口造数:最推荐,稳定且不依赖页面 DOM,还能避开页面前端校验
以我的经验,数据准备优先用接口,其次是数据库,最后才是页面操作。因为客户系统的页面改动很频繁,你辛辛苦苦写好的页面操作说不定一个月后就失效了,而接口一般会比较稳定。不过用接口造数有一个前提:你必须拿到接口文档或抓包数据,并且要知道哪些参数是必填的,哪些是可以随便传的。
4.1 测试账号与数据隔离,没管好这层会反复崩溃
客户环境里最常见的现象是:一套回应用户账号被好几个人共用。你做你的用例,他跑他的脚本,谁都没跟谁说,结果就是账号的登录态、业务数据、功能配置全乱套了。
测试账号必须按“一人一账号”或“一套任务一个账号”的原则来管。如果客户环境做不到这一点,那就得在用例设计上做隔离:每个用例不能依赖“系统里当前必须有某条数据”,而是在前置步骤里自己把数据建出来。
这里面还有一个非常重要的细节:测试账号的密码不要明文写死在脚本里。客户环境里安全要求高,脚本文件如果泄露给无关的人,整个系统的核心业务就可能被随意操作。建议用环境变量或者一个 gitignore 的配置文件来存密码,脚本里从配置读取。
4.2 用例前后端数据清理的经典写法
下面这个例子是我在客户项目里常用的“建数据-跑流程-清数据”模式。核心思路是在用例开始前通过前端页面创建一个测试单号,用例跑完后再通过页面把它删掉,保证系统里不残留脏数据。
def create_issue(driver, title, content): driver.find_element(By.ID, "newIssueBtn").click() wait = WebDriverWait(driver, 10) wait.until(EC.visibility_of_element_located((By.ID, "issueTitle"))).send_keys(title) driver.find_element(By.ID, "issueContent").send_keys(content) driver.find_element(By.ID, "saveBtn").click() # 等待保存成功提示 wait.until(EC.visibility_of_element_located((By.CLASS_NAME, "toast-success"))) def delete_issue(driver, issue_id): driver.get(f"https://customer.example.com/issue/delete/{issue_id}") alert = driver.switch_to.alert alert.accept() time.sleep(1) # 这里留一点点缓冲让操作生效 def test_issue_flow(): issue_id = f"auto_{int(time.time())}" title = f"自动化测试工单-{issue_id}" try: create_issue(driver, title, "这是一个临时测试数据,跑完自动清理") # 中间流程省略... finally: # 无论用例成功还是失败,都要清理数据 delete_issue(driver, issue_id)注意一个关键点:数据清理必须放在finally里,不能放在用例正常路径的最后。因为如果中间断言失败,脚本直接抛异常,后面的清理代码就不会执行了。用try...finally可以把“无论成功失败都要做的事”固定下来。
4.3 权限和环境隔离,客户项目与内部项目最大的不同
在客户项目里还有一个非常现实的问题:你没有客户生产环境的完全控制权。你需要数据库权限,可能要等客户 DBA 审批;你需要操作某些核心业务单据,可能要借客户业务人员的账号;你需要修复一条脏数据,客户可能让你提交变更申请。
这意味着,Selenium 用例的运行环境不是一个“你说了算”的地方。基于这个现实,我在客户项目里总结了几条铁律:
- 不要在客户生产库上直接执行 delete/update 语句,除非客户明确授意
- 不要让自动化用例去修改对真实业务有影响的开关配置
- 跑用例前先确认当前时段是否会影响客户正常业务(比如月末对账期间就别跑订单类用例)
- 用例执行频率要避开客户的批量作业时间窗口,否则两边互相锁表
这些虽然不是纯技术问题,但对客户项目的存活至关重要。哪怕是再熟练的 Selenium 工程师,如果在数据管理上掉以轻心,也很可能把客户项目搞黄。
5. 面向客户验收的用例组织与报告输出
写客户项目有一个特点:你不仅要对技术负责,还要对交付负责。客户不会去看你的代码写得多优雅,他们会看“你测试过哪些功能”“是不是所有功能都覆盖到了”“如果出问题了怎么证明”。所以,用例组织和报告输出必须从客户视角来设计,而不是从代码工程角度来设计。
一个很容易犯的错误是:测试工程师按自己的技术习惯把用例组织成“登录模块、订单模块、支付模块”,每个模块下面塞几十条细粒度用例。从技术上没问题,但客户做验收时,他们关心的是“从用户登录到下单支付这个完整的业务流程走通了没有”。项目答辩或交付汇报时,客户更愿意看到的是“我跑通了几个完整的业务场景”,而不是“我覆盖了多少个函数”。
所以在客户项目里,我推荐用例按“业务场景”来组织,而不是按“功能页面”来组织。举个例子:
- 场景一:新用户注册 → 登录 → 完善个人信息 → 退出登录
- 场景二:老用户登录 → 搜索商品 → 加入购物车 → 提交订单 → 在线支付 → 查看订单状态
- 场景三:管理员登录 → 后台审核订单 → 发货 → 用户端确认收货
这种组织方式最直接的好处是符合客户的认知逻辑。Selenium 用例跑完之后,你拿一张“场景执行情况表”给客户看,客户能一眼就看明白哪些核心流程是通的,哪些环节还有问题。
5.1 报告里必须包含截图和错误上下文
Selenium 脚本跑的时候,有些错误是偶发的、时序性的,比如某次页面响应慢了 3 秒,导致等待超时。如果报告里只有一条TimeoutException的堆栈信息,客户根本没法定位问题,你自己两天后再看也可能想不起来当时发生了什么。
所以,我写 Selenium 项目时,每个关键操作失败都会自动截图并保存当前页面的 HTML 源码。截图能直观看出页面上发生了什么;HTML 源码能让你在页面已经跳转后重新分析当时的 DOM 结构。
下面是一个通用的截图与日志封装:
import os import time from datetime import datetime from selenium.webdriver.support.events import AbstractEventListener class ScreenshotListener(AbstractEventListener): def __init__(self, log_dir="screenshots"): self.log_dir = log_dir os.makedirs(log_dir, exist_ok=True) def on_exception(self, exception, driver): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") # 截图 shot_file = os.path.join(self.log_dir, f"fail_{timestamp}.png") driver.save_screenshot(shot_file) # 保存页面 HTML html_file = os.path.join(self.log_dir, f"fail_{timestamp}.html") with open(html_file, "w", encoding="utf-8") as f: f.write(driver.page_source) print(f"[异常截图] {shot_file}") print(f"[页面源码] {html_file}")用了这个监听器之后,即使脚本在无人值守状态下跑挂了,你也能通过截图和 HTML 快速定位问题,不用每次等客户帮忙复现。
5.2 用例失败自动重试:治标与治本要分清
Selenium 客户项目里最让人头疼的问题是“偶发失败”。同一个用例,今天跑过了,明天跑挂,后天又过了。这种偶发性通常源于网络波动、页面性能抖动、后台定时任务冲突等等。这时候,很多团队会直接给用例加失败重试机制。
失败重试本质上是一种“容忍偶发失败”的手段,它解决了“跑挂后人工去重试”的繁琐,但也会掩盖真实的产品缺陷。比如某一个按钮点击后功能根本没实现,如果用例重试了三次还是失败,最终还是会被发现。但假设前端有一个 bug,触发概率只有 2%,一次性跑 100 条用例,有两条失败了,加个重试机制它们可能就过了,你说这算不算问题被掩盖?
我的建议是:重试机制要加,但重试结果和首次失败记录都要保留在报告里。不能让失败变成“重试后成功”就被当成什么都没发生。正确做法是做一个双层标记:
| 执行结果 | 报告显示 | 说明 |
|---|---|---|
| 首次失败,重试成功 | 黄色警告 | 说明稳定性有隐患,需要跟进 |
| 首次就成功 | 绿色通过 | 正常 |
| 重试仍然失败 | 红色失败 | 真正的缺陷,需要重点分析 |
5.3 客户项目的报告要以“人话”为主
做客户交付时报告还有一个特别容易忽视的点:不要全是英文技术堆栈。客户现场的负责人可能是业务专家,不一定是技术专家。你给他看一堆TimeoutException、ElementClickInterceptedException的堆栈,他除了懵圈,还会觉得你的自动化做得不够好。
真正的做法是在报告里加入“业务化描述”,即把每次失败转译成业务人员也能看懂的语言。例如:
- “点击保存按钮时,页面出现了一个非预期的弹窗,导致保存操作没有执行”
- “登录之后,系统页面没有在 15 秒内加载出首页数据”
- “订单查询条件中的日期控件无法输入内容,输入框被只读属性锁定”
这种描述方式的核心是:先告诉客户发生了什么业务问题,再附上技术细节供开发工程师排查。如果客户项目里已经有 JIRA 这样的任务管理工具,报告还可以把失败用例直接关联到一个测试任务上,客户团队在任务看板里就能看到失败原因。
Selenium 项目到后期,其实拼的不是谁写的定位表达式更花哨,而是谁的报告能让多方角色(客户、业务、开发、测试)都看懂、都能用。能把这一点做好,你的 Selenium 客户项目不仅有技术深度,还有交付价值。
6. 客户现场踩坑实录:三个典型问题的完整排查链路
最后这部分我想直接复盘几个我在客户现场真实踩过、也真实被坑过的典型问题。直接给答案没意思,因为下一次遇到的细节可能完全不同,但“排查链路”是可复制的。你能学会这套链路,再遇到类似问题就知道从哪儿下手。
6.1 点击保存按钮没反应,脚本却不报错
现象:点击“保存”按钮后,页面没有任何变化,既没报错,也没跳转,脚本继续往后跑然后在下一次定位时超时失败。
当时我的第一反应是定位不对,但浏览器手动操作明明是好的。后来我在脚本里加了点击前后的页面对比,发现是点击后有一个非常短暂的遮罩层出现又消失,挡住了按钮,导致 Selenium 的 click 实际上落在了遮罩层上。
企业级前端框架经常会在表单提交时添加一个全局遮罩,防止用户重复提交。Selenium 的点击是真实的鼠标事件,它不关心你页面上有没有东西挡着,只要它计算出来元素坐标,就按下去了。但如果那一个瞬间遮罩层恰好盖住了按钮位置,点击就被遮罩层拦截了。
排查链路:
- 先在失败现场截图,看按钮上是否有半透明遮罩
- 再用
driver.execute_script("arguments[0].click();", btn_el)做一次 JS 点击,如果 JS 点击能成功,基本可以确定是遮挡问题 - 优化方案:在点击前先等待遮罩消失(
invisibility_of_element_located),或者用 JS 点击绕开遮罩
6.2 元素在页面上肉眼可见,但 Selenium 一直定位不到
现象:打开客户系统的某个报表页面,肉眼清晰看到“导出”按钮,但 Selenium 用 id 定位就是找不到,报 NoSuchElementException。
这类问题非常有迷惑性。第一反应往往是定位表达式写错了,但仔细检查并没有,id 完全一致。真正的原因通常有三个方向需要排查:
- 元素在 iframe 中,需要切换 iframe
- 元素在 Shadow DOM 中,普通的
find_element进不去 - 页面打开了多个标签页或弹窗,Selenium 当前焦点不在包含该元素的标签页上
我那次遇到的是 iframe 问题。排查思路:先在失败时的页面源码里搜索按钮的 id,确认元素确实在 DOM 中;再检查它所在的父级节点是不是 iframe;如果是,切换进入后再定位。只要确认是 iframe,问题基本就解决了。
6.3 用例偶发超时,重试后又通过
现象:一个查询用例,每天跑 5 次,偶尔有一次在等待查询结果时超时,手动打开页面也是正常的,下一次跑又好了。
这种问题最磨人,因为它不稳定复现。我的排查方法是从时间维度入手:把每次失败的精确时间点和客户系统后台的定时任务时间表做对比,最后发现失败时间段恰好落在客户系统每天凌晨的数据归档任务窗口里。归档任务期间,数据库压力大,查询接口响应时间飙升,Selenium 这边 15 秒的等待阈值就被超过了。
这类问题的根因不在 Selenium,而在系统本身的性能和任务调度。真正要做的不是无限调大等待时间,而是调整用例执行计划,避开客户系统的批处理窗口,并在报告中记录这个特殊时段,避免后续再用例在这个时间段跑。
排查链路总结成一句话:先把问题固定在“是否能稳定复现”上,再用时间维度、网络维度、上下文维度去切分,定位到根因之后再做处理,而不是一上来就“加长等待时间”这种看似简单的表面修复。
7. 客户项目里 Selenium 用例的长期维护:从“能跑”到“经得起跑”
Selenium 客户项目里,最容易被低估的是长期维护成本。很多团队把自动化测试项目交付给客户后,前两周跑得挺顺畅,第三周开始逐渐出现失败,一个月后就没人再用了。这里面最核心的原因不是技术方案不行,而是“用例与真实系统之间的漂移”没有建立应对机制。
所谓漂移,就是前端页面上一次小改动,对于人来说可能无所谓,但对 Selenium 来说可能是致命伤。一个 class 名换了、一个 id 变了、一个按钮换了位置,都会导致用例失败。客户系统一个月发一次版本,很多时候新版发布后自动化用例就大面积失败,需要一两天来修复。
我在这个项目里使用的应对思路是“三层防护”:
页面对象模型(Page Object Model):把页面上的元素定位封装成一个类,用例层只负责业务操作,不接触具体定位。前端改版时,只需要改页面对应类里的定位,不需要把几十条用例全部改一遍。这是 Selenium 项目中性价比最高的架构设计,没有之一。
公共元素库:把登录、退出、弹窗关闭这些复用度极高的操作做成公共方法,前端改一次,全局生效。
定期巡检:客户系统发布新版本之前,主动在测试环境跑一遍 Selenium 套件,把新版本可能带来的问题提前暴露出来,而不是等客户生产环境崩了再补救。
用页面对象模型设计之后,用例代码和页面结构之间形成了一道缓冲。同样是客户系统改版,没有 POM 的用例可能要改十几个文件,有 POM 的用例只需要改一个LoginPage.py,半天时间就能完成适配。这就是长期维护价值的体现。
8. 给即将接客户 Selenium 项目的人的几点私人建议
最后写点不那么“技术”,但却是这几年做客户项目沉淀下来的真心话。这些经验看起来琐碎,但在客户现场往往比技术本身更管用。
第一,永远先问清楚客户环境的约束条件,再谈方案。包括浏览器版本、网络权限、数据库权限、发布频率、数据敏感度、跑批窗口。这些信息越早确认,项目后期的坑越少。
第二,把 Selenium 脚本当成一个产品来做,而不是一堆代码的堆砌。脚本要有清晰的目录结构,要有日志,要有截图,要有可配置的等待时间,要有能看懂的报告。这些东西在你自己本地跑可能觉得没必要,但对客户项目是必需品。
第三,没有“永远稳定”的自动化测试,只有“及时修复的自动化测试”。客户项目里 Selenium 用例出现失败不可怕,可怕的是失败之后没人去看、没人去修,最后整个套件慢慢腐烂,变成“跑不动的废项目”。你需要在交付时就跟客户明确,自动化测试的维护是一个持续投入的过程,而不是一锤子买卖。
第四,不要在客户现场尝试用技巧去“骗过”系统的防自动化机制。比如识别到 selenium 标识后切换到真实浏览器来跑,这种操作在客户项目里有很严重的合规风险,而且一旦被发现,你失去的可能不只是这个项目。做自动化测试,核心价值是提升回归效率、验证业务流程,不是去攻破什么防护,这个底线要守住。
第五,一定要有备份和版本管理。Selenium 脚本会被反复修改,没有 Git 管理几乎等于裸奔。改崩了一次,没有历史版本可以回退,客户那边又急着要结果,那种崩溃我只经历过一次就不想再经历第二次了。