做WebUI自动化,Selenium几乎是绕不开的第一选择。它直接拉起真实浏览器,模拟真实用户的点击、输入、滚动和等待,相比纯接口校验多了一层“端到端”的直观感,也因此成了很多测试团队搭建UI自动化体系的首选框架。可真正上手之后你会发现,Selenium写demo很容易,写上几十条用例还能稳定运行却很难。
明明同一个操作,本地跑得顺顺利利,一上CI就随机飘红;昨天全绿的用例,今天突然告诉你元素找不到;更常见的是点击按钮没有反应、登录后进不去下一页、下拉框选项死活选不上。这篇文章就用一个从业者的视角,把Selenium WebUI自动化里最常用的API按场景拆一遍,再把我自己踩过的和帮别人排查过的10个高频问题记录下来。适合正在写Selenium脚本的测试开发,也适合刚从接口测试切到UI自动化的同学照着避坑。
1. 先把地基打牢:WebDriver、驱动管理与API全貌
1.1 一个能稳定运行的WebDriver实例长什么样
很多人第一步就栽在环境上。Selenium本身只是一个客户端库,真正干活的是浏览器里的驱动进程。拿Chrome来说,你需要下载和浏览器版本严格匹配的chromedriver,然后在代码里通过WebDriver去拉起它。我第一次遇到“SessionNotCreatedException”时还以为是代码问题,后来才发现是驱动和浏览器版本差了三个小版本。
比较稳妥的做法是用工具自动管理驱动。我自己习惯在本地用一个简单的脚本去读浏览器版本,再下载对应的驱动放到固定目录;在团队工程里则建议把驱动版本写死在配置里,配合CI镜像固定浏览器版本,避免今天能用明天不能用。这里有个容易踩的坑:不要把driver的可执行文件直接提交到代码仓库,即使你用的是Windows下的exe。驱动更新非常频繁,仓库里躺着旧驱动只会让问题更难追踪,更推荐在部署阶段统一安装。
创建WebDriver实例时,有一些选项会直接影响后续脚本的稳定性:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() # 关闭"Chrome正受到自动软件控制"的提示条,避免遮挡页面顶部元素 options.add_experimental_option("excludeSwitches", ["enable-automation"]) # 禁用扩展,避免浏览器插件改变页面结构 options.add_argument("--disable-extensions") # 禁用GPU,在无头模式或虚拟机上减少渲染层面的随机问题 options.add_argument("--disable-gpu") driver = webdriver.Chrome(options=options) driver.set_window_size(1920, 1080)固定窗口大小非常关键。很多自动化用例在窗口大小不一致的环境里表现完全不同,尤其是依赖悬浮菜单、侧边栏布局的页面。我见过一个团队在本地1920宽度的屏幕上跑得好好的,到了服务器上因为默认窗口只有800宽,导航按钮被折叠进了汉堡菜单,用例直接全挂。所以初始化时set_window_size这一步,建议作为标配写上。
1.2 元素定位API:八种定位方式该怎么选
Selenium提供了八种定位方式,它们统称为By:id、name、class name、tag name、link text、partial link text、xpath、css selector。说实话,日常维护脚本时我几乎只用两种:id和css selector,xpath作为兜底。
为什么优先id?因为id在页面里通常是唯一的。一个页面能有个稳定的id,定位成本就是最低的。但很多现代前端框架生成的id是动态的,比如“app-12345”这种带随机数的,这时候再用id就是自己给自己挖坑。
css selector适合有清晰class结构和层级关系的元素,比如“.btn-primary”、“#login-form input[type='submit']”。它比xpath简洁,执行性能也更好。不过css selector不支持按文本内容定位,遇到“按钮文字是‘登录’”这种需求就只能用xpath。
xpath最大的价值在于两点:按文本找元素、按父子兄弟关系找元素。前端页面如果连class都频繁变动,xpath里的相对路径通常还能顶一阵。但要强调的是,不要复制浏览器自动生成的绝对xpath,那种/html/body/div[1]/div[2]/div[3]/…的路径,前面哪怕多一个div都会崩。写xpath时我习惯尽量用相对路径加属性条件,比如//form[@id='login-form']//button[contains(text(),'登录')],这样中间层级怎么变都不受影响。
还有一类元素定位需要注意:可能同时存在多个或零个匹配。默认的find_element遇到多个匹配时,日志不会报错,它只是静默地返回第一个。这个“静默”往往就是bug的来源。团队成员在页面上看到的是第二个匹配,而脚本拿的是第一个,两边各说各话。解决方法是圈定范围:先定位到表单或者卡片容器,再在容器内find_element,保证定位结果的唯一性。
1.3 常用交互API的分组记忆法
交互类的API其实可以分成四组,这样记起来不会乱:
- 输入类:send_keys负责填文本,clear()负责清空。有个细节,很多输入框预设了灰度值,直接send_keys不会覆盖已有内容,需要先clear再send_keys,否则文本会拼接在一起。极端情况下输入框有格式化逻辑,比如手机号按3-4-4分段,清空后还要逐个字符慢慢输入,这就涉及后面要说的复杂交互。
- 点击类:click()是使用频率最高的API。但click有一个限制,它要求元素必须可见、可点击,有些场景下需要先用ActionChains做鼠标移动,或者用JavaScript强行点击。这个后面会展开讲。
- 表单类:submit()已经很少用了,多数情况下直接在表单内找到提交按钮click()更符合真实用户操作。下拉框要用Select类,它支持select_by_index、select_by_value、select_by_visible_text三种选择方式。
- 状态类:is_displayed()、is_enabled()、is_selected()用来做点击前的状态判断。注意is_displayed()只能判断元素是否在DOM中且占据空间,不能判断是否真正可点击。
这四组API看着简单,但在真实页面里遇到的组合问题远比单个API复杂。举个例子,一个按钮在用户输入完必填项之前是disabled状态,如果你一上来就click(),元素定位成功但点击无效,异常信息还不给你任何提示。所以很多高级教程都在强调“预期条件”这个概念,本质上就是让你在交互前先确认状态。
2. 决定脚本稳定性的三个关键API模块
2.1 等待体系:显式、隐式、固定sleep的正确取舍
我一直认为,等待是WebUI自动化里最值得花时间研究的模块,因为大部分“莫名其妙”的失败都源于等待不当。Selenium里等待有四种常见手段:固定sleep、隐式等待、显式等待、强制等待(如等待某个元素消失的条件)。
固定sleep(time.sleep(2))简单粗暴,但它的缺点是“无论页面快还是慢,我都雷打不动等两秒”。页面1秒就加载完,你白等1秒;页面5秒才加载完,你照样失败。整个测试集如果有一百个用例,每个用例多等两秒,累积时间是非常可观的。我的建议是:固定sleep只能作为最后的兜底,而且时间要短,比如等待某个动画结束的0.5秒。
隐式等待是通过driver.implicitly_wait(10)设置的全局等待,它的工作机制是每次find_element找不到元素时,会在设定时间内反复轮询。它看起来省事,但有一个很明显的副作用:当元素确实不存在时,你要等满整个超时时间才能拿到NoSuchElementException,排查时非常拖节奏。更麻烦的是,隐式等待和显式等待混用时,两者的超时时间会叠加,导致有些用例异常慢。所以我个人的习惯是二选一,优先用显式等待。
显式等待是WebDriverWait配合expected_conditions(简称EC)的写法,它能针对单个条件进行轮询,比如“等这个按钮可点击”“等这个元素可见”“等这段文本出现”。我贴一段最常用的写法:
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, 10) login_btn = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'登录')]"))) login_btn.click()这段代码的意思是“最多等10秒,直到登录按钮变得可点击”。10秒内只要条件满足就立刻放行,效率高,逻辑清晰。实际项目中可以把wait封装成工具函数,比如wait_clickable(locator)、wait_visible(locator),整个测试框架的可维护性会好很多。
在等待这里我还想提一个经验:等待的条件最好和下一步操作强相关。比如下一步要点击按钮,就等“可点击”,而不是等“存在”。存在和可点击之间还隔着CSS、覆盖层、动画好几层状态。等错条件,等于白等。
2.2 执行JavaScript:绕过Selenium限制的万金油
driver.execute_script是Selenium里一个被很多人低估的API。它的作用是在当前页面上下文里执行一段JavaScript,可以直接读取和修改DOM属性,也可以触发某些事件。用它能解决很多Selenium原生API搞不定的事。
举几个我实际用过的场景。第一个是处理被遮挡的元素。有时候一个按钮被固定定位的浮层挡住了,Selenium的click()会抛提示说element not interactable。但真实用户是可以点击的,因为它只是视觉上被遮,点一下还是会点到按钮,只是点不到浮层上。这时候可以用execute_script("arguments[0].click();", element)直接触发点击事件,绕过可见性检查。不过我要提醒一句:这个方法适合“已知元素存在且真的要操作它”的场景,如果元素本身不可见或者没渲染完成,强行用脚本点击只会制造更隐蔽的问题。
第二个场景是滚动。Selenium没有原生的scrollTo方法,但它可以通过execute_script("window.scrollTo(0, document.body.scrollHeight);")滚到底部,或者把某个元素滚进可视区域:arguments[0].scrollIntoView(true)。滚到可视区域这个操作在长列表页面里非常常用,比如你要点击页面底部的“提交”按钮,如果不先滚动,按钮虽然存在于DOM中,但浏览器不会让你点击。
第三个场景是处理复杂输入框。有些组件,比如日期选择器或富文本编辑器,原生的setValue和clear()经常失灵。这时候可以用JS直接给input框赋值,再派发一个input事件,让前端框架感知到值的变化。这类写法不同框架略有差异,但基本思路是一样的:直接操作DOM属性,再手动触发事件。
值得强调的是,execute_script虽然万金油,但它能跳过Selenium的很多检查机制,用不好会让问题更隐蔽。我的原则是:优先用原生API,原生API搞不定再上JS,上JS的时候必须写清楚为什么要这么干,避免后来者盲目复制。
2.3 iframe、多窗口、弹窗切换:顺序比API更重要
WebUI自动化里经典的“三步走”问题:iframe怎么进、多窗口怎么切、弹窗怎么处理。这三个问题单独拎出来都不难,难的是切换顺序。很多人脚本跑着跑着突然报找不到元素,一看,其实是当前上下文已经不对了。
处理iframe时,最常用的API是driver.switch_to.frame(参数),参数可以是iframe的id、name、索引,也可以是WebElement。进入iframe之后,后续的find_element都在这个iframe内部查找;等操作完毕,一定要切回默认内容:driver.switch_to.default_content()。我印象中翻车最多的就是嵌套iframe,一层进一层,退的时候直接退多了或者退少了。建议每次进入iframe前用变量记录当前层级,退出时逐层退,而不是直接跳到最外层。
多窗口切换的套路是固定的:点击一个链接打开新标签页,此时要用window_handles拿到所有窗口句柄,找到新窗口的句柄,然后switch_to.window(new_handle)。这里最容易忽略的一点是,点击链接到新窗口打开之间是有延迟的,直接马上拿window_handles可能只拿到一个句柄。所以切换前最好用WebDriverWait等window_handles的数量从1变成2,这个等待条件用EC.number_of_windows_to_be(2)就能实现。
弹窗部分要区分alert弹窗和页面内的模态框。alert弹窗用driver.switch_to.alert处理,配合accept()和dismiss();页面内模态框本质上是页面元素,直接定位操作即可。很多新手把两者混为一谈,看到弹窗就去处理alert,结果没有alert,反而把自己搞晕。判断依据很简单:如果是浏览器级别的弹窗,WebDriver无法用find_element去定位,只能走alert API;如果是页面自绘的弹窗,一定能在DOM里找到对应节点。
还有一个常见细节:switch_to操作本身不会告诉你现在在哪个上下文。调试时可以用driver.current_window_handle和driver.page_source确认当前状态。我在排查问题时经常打印这两个值,比自己瞎猜快得多。
3. 实操复盘:一个登录用例从设计到落地的完整过程
3.1 最小可运行工程与依赖管理
前面讲了很多API,现在把整个过程串起来。我以最经典的登录场景为例,跑一遍从工程搭建到用例断言的全流程。
工程依赖不用复杂到引入一套完整框架,先搭一个最小的可运行结构就好。我用Python举例,目录大概是这样:
login_demo/ ├── config.py # 全局配置:地址、账号、超时时间 ├── utils.py # 等待、截图、日志等工具函数 ├── test_login.py # 登录用例 └── requirements.txt # selenium、pytest、webdriver-manager依赖安装用pip,驱动管理用webdriver-manager库可以省掉手动下载驱动的麻烦。这个库会自动检测本地浏览器版本并下载匹配驱动,对团队新人也友好,不然每次换环境都要解释一遍chromedriver该去哪下。
pip install selenium pytest webdriver-manager在config.py里把环境地址、账号密码、超时时间都拆出来。账号密码这种敏感信息建议从环境变量读取,而不是硬编码在仓库里,尤其当仓库会被多个角色看到的时候。环境变量可以是系统环境变量,也可以是pytest的fixture注入,灵活处理即可。
3.2 步骤设计:把用户行为翻译成WebDriver指令
一个正常用户登录,行为顺序是:打开登录页,输入用户名,输入密码,点击登录按钮,等待跳转到首页,确认登录成功。翻译成WebDriver指令,对应的就是get、send_keys、click、wait。
这里的关键动作是把每一步都加上“目的注释”。比如输入用户名之前,先等用户名输入框可输入;点击登录之前,先等登录按钮可点击。很多人在写用例时只写了操作,没写前置条件,导致页面慢一点就失败。我习惯给每个核心操作都加一步显式等待,让用例天然具备“抗慢”能力。
from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from config import BASE_URL, USERNAME, PASSWORD def test_login_success(driver): driver.get(BASE_URL + "/login") username = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "username")) ) username.clear() username.send_keys(USERNAME) password = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, "password")) ) password.clear() password.send_keys(PASSWORD) login_btn = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'登录')]")) ) login_btn.click() # 登录后等待跳转,用登录成功标志元素出现来确认 WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CLASS_NAME, "welcome-msg")) )这个用例本身并不复杂,但已经把前面提到的等待体系、定位策略都用上了。在真实项目里,登录还经常有验证码、短信验证、多因素认证等环节,这些通常需要在测试环境中做开关处理,比如某家公司会配置一个测试专用的万能验证码。此类信息需要跟开发和运维确认,不要在脚本里写死某个一次性验证码。
3.3 断言设计:验证什么才算真的通过
用例的最后一步是断言。很多人习惯断言“页面URL变成了首页地址”,或者断言“页面上出现了‘欢迎你’这几个字”。这两种断言都有用,但要讲究优先级。
URL断言比较脆弱,因为现在很多前端路由是单页应用,URL的变化不意味着页面内容渲染完成,而且URL里偶尔带着随机参数,写起来要小心正则匹配。文本断言稍微好一点,但还是建议选择有业务含义的元素,比如登录后显示用户昵称的节点,或者首页的核心功能入口。
到这里,登录用例已经完整。但这个用例在别人电脑上跑不跑得起来,取决于你的fixture和conftest设计。pytest里可以用conftest.py定义driver的fixture,这样每个测试用例自动获得一个浏览器实例,结束时自动关闭,还能在失败时自动截图。这部分在工程化章节会展开。
4. Selenium WebUI自动化十大高频问题实录与排查
4.1 高频问题1到3:定位不到、点击穿透、元素不可交互
现在进入整篇文章的高潮部分:十大高频问题。这十个问题不是从网上抄来的,是实际排查中反复出现的。每个问题我会讲现象、原因和解决办法,并且给出排查的顺序。
问题一:NoSuchElementException,元素定位不到。这是所有Selenium用户遇到的第一个坎。现象很直白:findElement抛异常,说找不到。但原因可能有五种:页面还没加载完、元素在iframe里、元素是动态渲染的、定位条件写错了、元素确实不存在。我的排查顺序是:先用浏览器开发者工具手动确认元素在不在;在的话,看它是不是在iframe里;不在的话,前面加显式等待试试;还不行,就把定位表达式简化,用contains或基于文本找。这一套下来,已经能解决九成的情况。
问题二:ElementClickInterceptedException,点击被拦截。现象是元素明明找到了,click()却抛异常,提示有其他元素接收了点击。最常见的原因是页面上浮着一个遮罩层或者广告弹窗,挡住了目标按钮。解决方式有三种:一是等遮罩层消失,比如等待某个loading元素visibility_of_element_located变成不可见;二是用ActionChains先移动到元素上再点击;三是用JavaScript强制点击(前面已经提过,要慎用)。我优先推荐第一种,因为它是从根上解决;后面两种属于绕路,绕路的问题在于等测试环境变化,坑还在。
问题三:ElementNotInteractableException,元素不可交互。这个和问题二容易混淆。问题二是被别的东西挡住,问题三是这个元素本身不能操作,比如元素是hidden状态、readonly状态,或者CSS上设置了pointer-events: none。遇到这种问题,先看元素属性,确认它当前是否可点击;再看它是否处于禁用状态,比如表单未填写完整时提交按钮是disabled。如果确认是业务逻辑必须等待的,就补等待条件;如果确认是前端临时隐藏的,就用JS改成可交互状态。一句话总结:先搞清楚是“不该点”还是“点不了”。
4.2 高频问题4到6:iframe、新窗口、下拉框与滚动
问题四:no such frame,或者明明进了iframe还是找不到元素。这个问题的根因通常是切换时机不对。iframe本身是异步加载的,当你用frame的id去切换时,iframe可能还没有插入到DOM中。解决办法是先用WebDriverWait等待iframe标签存在,再切换进去:
wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, "content-iframe")))这个API一步完成“等待+切换”,能省掉不少脑细胞。另一个根因是切进去之后忘记切回default_content,导致下一轮定位全部失败。遇到这种情况,建议在每个测试用例结尾统一清理上下文状态。
问题五:点击链接后新窗口没打开,或者打开了但切不过去。前面多窗口切换讲过,这里要补充一个实战细节:判断新窗口是否打开,不要用固定的sleep,要用wait。因为有些浏览器对弹窗拦截策略很严格,如果新窗口是target="_blank"打开的,而浏览器设置了拦截弹窗,窗口根本打不开。此时Selenium报的不是“找不到窗口”,而是window_handles一直只有1个。排查思路是去浏览器设置或浏览器配置里允许弹窗,或者在测试环境禁用弹窗拦截。
问题六:下拉框选项选不上,或者选上之后页面没反应。下拉框有两种:原生select标签和自定义下拉组件。原生的用Select类处理,自定义的要先点击下拉框打开选项列表,再点击对应选项文本。很多人拿原生的方法去处理自定义下拉框,自然选不上。判断方法很简单:看HTML里是select还是div/ul。遇到自定义下拉框,我通常建议用键盘操作:先点击触发下拉,然后用send_keys(Keys.ARROW_DOWN)选择并用send_keys(Keys.ENTER)确认。这个方式更接近真实用户操作,也避开了很多自定义组件事件绑定的坑。
4.3 高频问题7到10:文件上传、验证码、截图定位、慢操作
问题七:文件上传偶发失败。Selenium处理文件上传有两种情况:input[type=file]可以直接用send_keys把本地文件路径传给input;但是那些做成了自定义拖拽上传或点击按钮后再弹系统文件选择框的场景,Selenium默认是管不了的,因为系统文件选择框不在浏览器上下文里。普通场景直接用send_keys传路径就好,注意路径要绝对路径,相对路径经常解析不到。更高阶的方案是通过自动化工具去操作系统文件框,或者让前端开发留一个隐藏的input[type=file]用于测试注入,这个要跟开发协调。
问题八:验证码挡住自动化,用例无法稳定通过。验证码是自动化测试的痛点之一。我的建议分三步:第一步,跟开发确认测试环境是否有万能验证码或者验证码开关;第二步,如果没有开关,就在用例里识别到验证码图片时直接跳过该用例,避免误报;第三步,如果必须过验证码,就接入打码平台或者OCR识别。但这里要提醒一句,验证码本质上是安全机制,自动化测试不应该试图绕过它去验证业务,而是在测试环境关掉它或者屏蔽它,让自动化回归测试关注业务逻辑本身。
问题九:全屏滚动不稳定,页面元素总是没有进入可视区。用scrollTo滚到底部再回头定位元素,经常出现定位到了但元素又被折叠的情况。更稳定的做法是直接用scrollIntoView让目标元素进入可视区,然后再点击。有些长列表是懒加载的,滚动太快会触发不了加载,这时候需要分段滚动,每滚一段等数据加载完再继续。这类问题比较考验对业务页面行为的理解,没有万能公式,但“先滚到元素附近再细粒度操作”这个大方向基本不会错。
问题十:用例偶发失败,重跑就好了,但线上总在关键流程上报红。这是最让人头疼的一类问题。它的根源通常是资源加载不稳定、网络波动、或者前端在某一步有随机动画。排查思路是给用例增加更多上下文信息:失败时截图、抓URL、打印当前页面渲染的关键节点。如果发现失败集中在“等待时间刚好差一点点”的情况,适当调大超时时间,或者把单个超时改成轮询间隔更短的显式等待。实在不行再引入失败重跑机制,但重跑不能掩盖根本问题,只能作为兜底手段。
4.4 高频问题速查表
为了方便复制,我把这十个问题整理成一个速查表:
| 问题 | 典型报错 | 第一排查动作 | 兜底方案 |
|---|---|---|---|
| 定位不到 | NoSuchElementException | 开发者工具手动确认元素存在 | 显式等待 |
| 点击被拦截 | ElementClickInterceptedException | 检查遮罩层和弹窗 | JS强制点击 |
| 元素不可交互 | ElementNotInteractableException | 查看元素属性和disabled状态 | JS修改状态 |
| iframe进不去 | NoSuchFrameException | 等待iframe出现再切换 | frame_to_be_available |
| 新窗口切不过 | 找不到目标句柄 | 等待window_handles数量变化 | 检查弹窗拦截 |
| 下拉框选不上 | 点击无响应 | 确认select还是自定义组件 | 键盘选择 |
| 文件上传失败 | 无报错但上传不成功 | 确认input类型与绝对路径 | 隐藏input注入 |
| 验证码拦截 | 用例一直失败 | 测试环境开关或跳过 | 打码平台 |
| 滚动不稳定 | 元素不在可视区 | 用scrollIntoView | 分段滚动 |
| 偶发失败 | 重跑即过 | 收集上下文与截图 | 失败重跑机制 |
这张表每行的第一排查动作都能解决大概率问题,兜底方案是针对极端情况的备选。建议贴在项目文档里,队友遇到同样的报错先查表,能省掉很多无意义的讨论。
5. 工程化落地:从能跑到稳定跑
5.1 用例分层与失败重跑机制
单个用例能跑只是开始,真正考验人的是几百个用例在流水线上稳定跑。我见过不少团队,脚本写了一堆,但执行结果惨不忍睹,每天上班第一件事就是看哪个用例昨天又红了,然后一个个修。这种情况的根本原因是用例之间耦合度太高。
用例分层的核心是减少单体用例的依赖。登录用例只测登录,购物流程用例通过接口或者前置数据准备来跳过登录步骤,而不是在购物用例里又点击一遍登录。这样做的好处是,登录模块出了问题,只影响登录相关的少数用例,其他用例照常跑。
失败重跑机制是pytest-rerunfailures插件直接支持的,配置也很简单。但这里要提醒一点:重跑次数不要设太高,重跑1到2次就好,而且每次重跑之间最好清理浏览器状态,而不是复用同一个driver实例。如果重跑之后还是失败,说明这不是偶发问题,是真的bug或者环境问题,需要记录详细的失败现场。
5.2 日志、截图与报告:排查问题的三件套
稳定运行的另一个关键是给用例装上“记录仪”。一个用例失败时,如果只留下一句“找不到元素”,排查的人还是要对着代码猜半天。我在团队里推动了几条硬性要求:每条用例的关键步骤打日志;失败时自动截图;用例结束采集页面关键信息。
截图非常简单,driver.save_screenshot("failure.png")一行就行,但要把文件名加上时间戳和用例名,不然几十个截图互相覆盖。日志方面,建议在关键操作前后输出统一格式的日志,比如“步骤:输入用户名,状态:成功”,这样失败时能清晰地看到最后成功的是哪一步。报告方面,pytest自带的报告够用,再加一个pytest-html插件已经能应付大部分团队的诉求。
还有个很实用的小技巧:在失败回调里不仅截图,还可以把当前URL和当前窗口句柄都记录下来。有些问题就是环境串了导致的,比如上一个用例没有正常关闭干净,当前用例跑在了错误的窗口里,这时候看一眼句柄就能定位。
5.3 团队协作中的公共约定
最后聊一下团队层面的事情。Selenium自动化不是一个人的玩具,一旦多人协作,没有公共约定就会互相影响。
我们约定里比较重要的一条是:元素定位表达式集中在页面对象里维护,不要在测试用例里散落一堆By.ID。这样就避免了同一个按钮在十处代码里被写了十种定位方式,改版时改一处即可。第二条是:测试数据不要写死在用例里,新建订单、删除数据这类操作尽量通过接口完成,UI只做展示层的验证。第三条是:公共的等待时间、超时时间、截图目录都做成配置项,新来的同学不需要知道这些值为什么是这么多,直接改配置就能适配自己本地环境。
这些约定听起来很基础,但坚持下来之后,自动化的维护成本会下降一个量级。很多团队自动化死在“维护成本过高”,而不是死在“自动化本身不行”。工具链只是放大器,真正决定这套体系能跑多远的是你从一开始有没有控制住混乱程度。
我个人在实际操作中最深的体会是,Selenium WebUI自动化从来不是“写完脚本就完事”的活儿,它更像一个持续迭代的工程。今天能跑通不代表明天能跑通,前端的每一次改动都可能让某个定位失效。所以我的习惯是每次前端发版之后,抽时间跑一遍核心流程用例,把失败原因记录在案,能当天修的就当天修,绝不攒着。另外,遇到诡异问题先砍变量:把多余的等待去掉,把可变的路径固定下来,把浏览器恢复到干净的初始状态,你会发现很多问题都是环境串了或者状态污染,而不是Selenium本身出了问题。希望这份避坑清单能让你少踩几个我踩过的坑。