☰
Selenium常用函数实战指南:从元素定位到文件上传全场景覆盖
2026/10/12 5:31:48 网站建设 项目流程

我们去面试自动化测试岗位的时候,十次有八次会被问到同一个问题:Selenium 里你最常用的函数有哪些?这个问题看着简单,但回答得好的候选人其实不多。多数人只能说出 find_element 和 click、send_keys,再往深里问——元素带放大镜图标框不住怎么办、页面弹窗不是 input 标签怎么传文件、窗口切换为什么总是时灵时不灵——就开始含糊其辞。说到底,是平时写脚本只图“能跑”,没把 Selenium 这套 API 的用法体系化梳理过。

这篇东西我想换个写法,不按文档顺序平铺,而是按真实项目中遇到最多的高频场景,把 Selenium 常用函数拆开揉碎讲一遍。从八种元素定位的取舍、等待机制的底层逻辑,到文件上传的三种处理姿势、iframe 和窗口切换的坑,再到 JS 执行、键盘鼠标操作、数据断言这些实战必备技巧。目标就一个:你把这篇文章看完、练完,再遇到界面自动化的需求,手上能直接抄的解决方案至少覆盖九成场景。

这一系列文章前面几篇聊过测试理论、框架设计,这篇属于实战工具篇,适合正在学自动化测试的初级工程师、准备面试的求职者,以及想把自己的脚本写得更稳的进阶选手。内容会尽量口语化,中间的代码示例都用 Python 版本,Selenium 4.x 为主,部分地方会提到和旧版 3.x 的差异。

1. 元素定位:自动化脚本的地基,先把它打牢

Selenium 的所有操作都建立在“找到元素”这个动作之上。你写得再多技巧,定位这一步失败,后面全部白搭。所以我把这部分放在开头,而且会多花点篇幅去讲定位函数的选用逻辑——不是背八种定位方式的名字,而是搞懂每种方式适合什么场景、优先级怎么排、XPath 怎么写才不会“一天一小挂、三天一大挂”。

1.1 八种定位方式,实战中的优先级排序

Selenium 官方提供了 find_element 和 find_elements 两个基础方法,配合 By 枚举传入不同的定位策略。Python 代码长这样:

from selenium import webdriver from selenium.webdriver.common.by import By driver = webdriver.Chrome() element = driver.find_element(By.ID, "username") elements = driver.find_elements(By.CSS_SELECTOR, ".list-item")

By 下面支持八种定位方式:ID、Name、Class Name、Tag Name、Link Text、Partial Link Text、XPath、CSS Selector。但实际项目里,没有人会把这八种都用一遍,真正高频的其实就四种:ID、CSS Selector、XPath,外加少量情况下的 Link Text。我给一个自己在团队里定的优先级参考:

优先级定位方式适用场景注意点
第一选择ID登录框、搜索框、表单控件这类固定交互元素有些前端框架会动态生成 id,需要先看源码确认
第二选择CSS Selector结构相对稳定的页面元素、组合条件定位语法简洁,Selenium 执行效率高于 XPath
第三选择XPath无法用 id/class 唯一定位、需要根据文本内容定位灵活性强,但写不好会性能差、稳定性差
补充方案Link Text导航链接、按钮文字恰好是链接文本只能用于 a 标签
偶尔用Name/Class/Tag批量元素统计、低复杂度页面单独使用时命中率很低,不建议独立依赖

这个排法不是拍脑袋。ID 在 HTML 标准里就要求文档唯一,命中率天然有保证;CSS Selector 在浏览器的 document.querySelector 层面有原生优化,运行效率比 XPath 高;XPath 虽然慢一点,但它是唯一能按“元素文本内容”定位的方式,在某些场景里绕不开。Name 和 Class 的问题在于前端工程师经常会复用样式名(.btn、.input),一个 class 下面挂十几个元素是家常便饭,单靠它定位等于听天由命。

1.2 XPath 和 CSS 的写法进阶:不是越长越好

初学者最爱干一件事:打开浏览器的开发者工具,右键复制 XPath,直接把那一长串 //*[@id="app"]/div[3]/div/div[2]/form/div[1] 塞进脚本里。这种写法在页面刷新后大概率失效,因为加了点前端展示逻辑,dom 结构一变就跪。

写 XPath 的核心原则是“用属性,少用层级”。举个例子,要定位登录页面的用户名输入框,源码长这样:

<div class="form-group"> <label for="username">用户名</label> <input type="text" id="username" name="user" class="form-control" /> </div>

依次评估几种写法:

# 脆弱写法,三层节点依赖,改一个类名就挂 driver.find_element(By.XPATH, "/html/body/div[1]/div[2]/form/div[1]/input") # 相对好一点,但如果页面上还有其他 id 包含 user 的节点,仍然有风险 driver.find_element(By.XPATH, "//input[contains(@id, 'user')]") # 最稳妥的写法:直接锁定目标元素的必填属性 driver.find_element(By.XPATH, "//input[@id='username']") # 同样效果,边界更明确 driver.find_element(By.CSS_SELECTOR, "input#username")

另一个常见需求是“找某个文本对应的元素”,比如点击列表里文字为“删除”的按钮,XPath 可以这样处理:

driver.find_element(By.XPATH, "//button[text()='删除']") driver.find_element(By.XPATH, "//span[contains(text(), '确认删除')]")

contains(text(), ...) 写法有个需要注意的细节:如果目标元素的文本被拆到了多个子节点里,text() 取的是当前节点的直接文本,可能为空。这时候更稳的方案是用 .// 配合 normalize-space():

driver.find_element(By.XPATH, "//div[normalize-space(.)='保存并继续']")

CSS 这边的相对定位也可以玩出花,比如按属性前缀、后缀匹配:

/* 匹配 name 以 login 开头的元素 */ [name^="login"] /* 匹配 class 以 btn 结尾的元素 */ [class$="btn"] /* 匹配 href 里包含 product 的元素 */ a[href*="product"]

这些是必须掌握的。定位这东西,像交朋友,目标越清晰,关系越稳定——你要用“全名+唯一工号”去找人,而不是用“住在左边的那个穿红衣服的同事”这种描述。

1.3 定位不到元素?先查这五个原因

遇到 NoSuchElementException,先别急着改定位表达式。我每次排查都按下面这个顺序过一遍,命中率超过八成:

第一,元素是否在 iframe 里。这是最高频的坑。页面里嵌了 iframe 但脚本没有切进去,你再怎么写 XPath 都找不到。第二,元素是否在 Shadow DOM 里。前端组件库(比如某些自定义日历、富文本)会把内部元素包在 shadow-root 里,普通 find_element 是拿不到的。后面专门讲处理方案。第三,页面是否还没渲染完。不是页面 load 完了元素就在,很多 SPA 站点是异步接口返回后才渲染表单;需要等某个元素出现再操作。第四,元素是否被遮挡。有可能是弹层挡住,有可能是透明度遮罩覆盖,Selenium 可以找到元素,但点击时会报 ElementClickInterceptedException。第五,页面存在多份相同元素(比如两个窗口、两个 tab只关闭了当前句柄但旧句柄还活着),定位出来的不是你想操作的那个。简单排查法:先打印 driver.page_source 里的片段,或者确认当前窗口句柄,再决定下一步。

2. 高频交互函数:从输入点击到文件上传的完整弹药库

定位只是第一公里。拿到元素之后要做的所有事情——输入文字、点击、清空、选择下拉框、拖拽、上传文件——都依赖 Selenium 提供的交互函数。这里我会按真实项目中的使用频率来排序,把每个函数的应用场景、边界情况和容易踩的坑一起讲清楚。

2.1 文本输入与点击:看似简单,细节不少

最基础的三个方法:send_keys() 输入文本,click() 点击元素,clear() 清空内容。示例如下:

driver.find_element(By.ID, "username").send_keys("测试账号") driver.find_element(By.ID, "username").clear() driver.find_element(By.ID, "login-btn").click()

用起来不复杂,但有几个实际问题必须注意。第一个是 send_keys 的追加行为:如果元素里已经有默认值,市面上多数脚本的写法是直接 send_keys,结果就变成了“原有内容+新内容”。所以真正规范的流程是:先 clear(),再 send_keys()。有些前端组件的 clear() 不生效(比如封装了 React 受控组件的输入框),这时候可以用快捷键全选删除:先 ctrl+a 选中,再直接输入新内容覆盖。

from selenium.webdriver.common.keys import Keys element = driver.find_element(By.ID, "search-input") element.send_keys(Keys.CONTROL, "a") # 全选 element.send_keys("新的搜索词")

第二个是 click() 的失效场景。按钮上方悬着浮动层、元素不可见但存在、页面在点击瞬间发生重绘,这些都可能让 click() 不出效果。遇到的时候不要强行调用原生 click,而是用 JavaScript 触发点击兜底,代码在后面 JS 执行部分会给。第三个是文件上传类的 input 标签,send_keys 直接传本地路径即可,这个属于上传专门的高频场景,我在第四部分单独展开。

还有一个高频组合是表单提交。用户填完用户名密码,你当然可以直接点登录按钮,但更贴近真实用户行为的是按回车提交:

password = driver.find_element(By.ID, "password") password.send_keys("123456") password.send_keys(Keys.ENTER)

回车提交有一个隐含好处:能触发前端 form 的 submit 校验逻辑,你点按钮反而可能绕过某些非必填校验,导致测试场景失真。

2.2 下拉框与多选控件:select 对象的正确姿势

网页里的下拉框分两类。一类是原生<select>标签,另一类是 JavaScript 模拟的假下拉(div + 列表 + 点击事件)。两者处理方式完全不同,我分别讲。

原生 select 标签用 Selenium 的 Select 类处理,不仅代码干净,而且语义清晰:

<select id="city"> <option value="beijing">北京</option> <option value="shanghai">上海</option> <option value="guangzhou">广州</option> </select>
from selenium.webdriver.support.ui import Select city_select = Select(driver.find_element(By.ID, "city")) city_select.select_by_value("shanghai") # 按 value 属性选 city_select.select_by_visible_text("北京") # 按可见文本选 city_select.select_by_index(2) # 按下标选,从0开始

select_by_visible_text 在中文页面上最直观,我通常是首选。select_by_value 适合 value 属性有稳定业务含义的场景。select_by_index 排在最后,因为一旦前端在中间插入一个选项,脚本就得跟着改。

另外一个常见操作是读取当前选中的选项、或者校验回显值:

selected = city_select.first_selected_option print(selected.text) # 多选 select 用 all_selected_options multi_select = Select(driver.find_element(By.ID, "hobbies")) all_selected = multi_select.all_selected_options

遇到 JavaScript 模拟的下拉框,Select 类型就失效了——本质是把下拉点击开,等选项列表加载出来,然后精准点目标项。这类组件的选项项通常在 ul/li 或者 div 的结构里,用文本定位就特别顺手:

driver.find_element(By.CSS_SELECTOR, ".select-trigger").click() driver.find_element(By.XPATH, "//li[contains(text(), '上海')]").click()

2.3 键盘与鼠标事件:模拟更复杂的用户路径

有时候测试用例要求的不是“点一下按钮”,而是“双击单元格进入编辑”“右键呼出菜单”“拖拽滑块到指定位置”。Selenium 的 ActionChains 类就是干这个的。

键盘组合键在前面已经用过一个 Ctrl+A,这里补充几个常见场景。需要强调的坑:send_keys 带组合键时,Windows/Linux 用 Keys.CONTROL,macOS 上要改成 Keys.COMMAND。

from selenium.webdriver.common.keys import Keys # 回车 / Tab 切换焦点 element.send_keys(Keys.TAB) # 复制粘贴 element.send_keys(Keys.CONTROL, "c") target.send_keys(Keys.CONTROL, "v") # 回退删除 element.send_keys(Keys.BACKSPACE)

鼠标操作类场景,我用得最多的是这几类:悬停看下拉菜单、双击进入编辑状态、右键打开业务菜单、拖拽滑块做验证码或调节器。看一个鼠标悬停的例子:

from selenium.webdriver.common.action_chains import ActionChains user_menu = driver.find_element(By.CSS_SELECTOR, ".user-avatar") ActionChains(driver).move_to_element(user_menu).perform() dropdown_item = driver.find_element(By.LINK_TEXT, "个人中心") dropdown_item.click()

这里有个小坑:move_to_element 只移动鼠标,不触发点击展示菜单的逻辑一般是悬停后 CSS 控制的,所以先悬停,再定位菜单元素,这是固定套路。

拖拽操作使用 drag_and_drop。比如滑块验证码(简化场景):

from selenium.webdriver.common.action_chains import ActionChains slider = driver.find_element(By.CSS_SELECTOR, ".slider-btn") target = driver.find_element(By.CSS_SELECTOR, ".slider-target") ActionChains(driver).drag_and_drop(slider, target).perform()

如果你的拖拽目标是一个动态计算的距离,可以换用 click_and_hold + move_by_offset + release:

ActionChains(driver) \ .click_and_hold(slider) \ .move_by_offset(xoffset=180, yoffset=0) \ .pause(0.5) \ .release() \ .perform()

move_by_offset 里 xoffset 的位移数值需要自己先算:一般是缺口距离减去滑块宽度,而这个距离可以通过截图和像素对比大致估出来,自动化项目里常见做法是“滑过头了再回退几像素”。

ActionChains 还有一个高频应用:页面滚动到某个元素可见后再操作。普通场景用 driver.execute_script 直接滚,但有的元素需要先悬停才能正确位置,这时配合 scroll_to_element 也能解决问题——不过这个函数实操中表现不稳定,我更倾向于直接 JS 滚,这个在后面的 JS 执行部分细说。

3. 等待机制:脚本时灵时不灵的病根,在这里治

我见过太多脚本失败不是因为定位表达式错了,而是页面元素没就绪,脚本就急着去操作。Selenium 脚本跑不过夜,十次里有七次都是同步问题。这一章把三种等待讲透,后面你再遇到 Click 不生效、元素找不到这类问题,思路会清晰很多。

3.1 三种等待的对比:什么时候用哪个

Selenium 提供三种等待:强制等待(time.sleep)、隐式等待(implicitly_wait)、显式等待(WebDriverWait + expected_conditions)。先给结论:强制等待能不用就不用,隐式等待给个兜底,显式等待是核心方案。

强制等待就是写死 sleep(5),它的坏处不用多解释:页面快的时候白白浪费5秒,页面慢的时候5秒可能还不够。而且在机器负载高的时候,time.sleep 的误差很大,测试结果不稳定。

隐式等待是全局性的,只要设置一次,它会在每次 find_element 的时候轮询等待元素出现,默认 500ms 轮询一次。我一般这样设置:

driver.implicitly_wait(10)

注意:隐式等待只作用于元素查找,不作用于元素可点击、可见、包含文本等状态判断。另外,隐式等待配合显式等待使用时,如果全局设了10秒,显式等待也设了5秒,实际最长可能跑到接近15秒才报错,增加无谓的等待时间。所以很多团队干脆把隐式等待设短,重逻辑全部交给显式等待。

显式等待是对特定条件的精准等待,代码可读性强、逻辑表达准确。下面的写法是真正贯穿全部自动化项目的核心模式:

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 等元素可点击(比如弹窗按钮) WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "confirm-btn")) ).click() # 等元素可见 WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.CSS_SELECTOR, ".toast-success")) ) # 等元素消失(比如 loading 遮罩) WebDriverWait(driver, 10).until( EC.invisibility_of_element_located((By.CSS_SELECTOR, ".spinner")) )

显式等待配合的函数有很多:presence_of_element_located(元素存在于 DOM)、visibility_of_element_located(元素可见)、element_to_be_clickable(元素可见且可点击)、text_to_be_present_in_element(元素包含指定文本)等等。这些都是 Selenium 在 expected_conditions 里内置好的常用条件,比我拿到元素后自己轮询判断 else break 要稳定得多。

3.2 显式等待的正确用法:可别把等待函数本身写错

新手最容易犯的错是把 element 实例传进 EC 条件,而不是把定位元组传进去。看一个对比:

# 错误示范:传了元素本身,EC 条件每次判断时元素已过期就会抛异常 element = driver.find_element(By.ID, "username") WebDriverWait(driver, 10).until(EC.visibility_of(element)) # 正确示范:传定位条件,WebDriverWait 内部重新查找 WebDriverWait(driver, 10).until( EC.visibility_of_element_located((By.ID, "username")) )

element 和 locator 是两码事:前者是某个时刻查到的一个对象引用,后者是“如何重新找到这个元素”的描述。页面一刷新,之前的 element 就 stale 了(ElementNotInterruptableException 或者 StaleElementReferenceException),但 locator 永远有效。

还有个建议:把显式等待封装成通用方法,而不是在每一步都写一长串。自定义一个 wait_click、wait_input 之类的小工具函数,使用体验会好一个量级。比如这样:

def wait_click(driver, by, value, timeout=10): return ( WebDriverWait(driver, timeout) .until(EC.element_to_be_clickable((by, value))) .click() ) def wait_visible(driver, by, value, timeout=10): return WebDriverWait(driver, timeout).until( EC.visibility_of_element_located((by, value)) )

这样测试用例代码就变成了:

wait_click(driver, By.ID, "login-btn") wait_visible(driver, By.CLASS_NAME, "dashboard")

可读性不是好一点半点,而且统一处理了重试和时间开销。

3.3 自定义预期条件:内置条件不够用的时候怎么办

业务场景千变万化,内置的 EC 条件偶尔也覆盖不上。比如你要等到某个接口返回的特定文本出现在页面角落,用 text_to_be_present_in_element 肯定不够,因为那个文本根本不在任何元素里,你需要自己写一个条件。

自定义条件本质上是写一个函数,入参是 driver,返回值为真或假:

class page_has_text: def __init__(self, text): self.text = text def __call__(self, driver): return self.text in driver.page_source WebDriverWait(driver, 10).until(page_has_text("订单已完成"))

等价写法用 lambda 也能搞定,但类写法更清晰。实际项目里有个高频场景:等待某元素具备某个 CSS 属性值(比如按钮变灰到变亮),这种也可以封装成自定义 expected_condition。总之,WebDriverWait 的 until 内部只关心“函数返回是否为 True”,这就是它灵活的内在机制。

4. 文件上传:三种主流方案的完整求解

文件上传是自动化测试里被问得最多的高频场景之一,也是很多人觉得“怎么搞都别扭”的重灾区。原因很现实:上传控件的类型太多,有的原生支持 file input,有的需要打开系统文件选择框,还有的是拖拽/粘贴上传。只背一个 send_keys 的教程根本不落地,这里按三种情况分别给方案。

4.1 方案一:input 标签,一行代码解决

最长见的情况是网页里有一个<input type="file">。这是 Selenium 最容易处理的场景,因为 Selenium 不允许也不应该操作系统级窗口,但 file input 本身就是页面元素,直接把本地文件路径传给 send_keys 就行:

upload_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']") upload_input.send_keys("/Users/me/Downloads/test_report.pdf")

如果要传多个文件(input 标签配置了 multiple 属性),路径用换行符分隔:

upload_input.send_keys("/path/to/a.pdf\n/path/to/b.pdf")

这个方案的要点是:定位 input 标签的时候不一定非看到那个“选择文件”按钮,很多页面 input 是隐藏的(display:none),但这并不影响 send_keys。常规的 visibility 检查在这里不一定适用,如果用了 EC.visibility_of_element_located 反而会失败。正确判断直接用 presence_of_element_located 或者 element_to_be_clickable。

4.2 方案二:非 input 标签,先试试键击发送

并不是所有系统都让你直接操作 input。部分内网系统、老旧的 Java 上传组件、人脸识别类控件,或者是某些商业控件(webuploader、layui 封装后的上传)根本不是普通 input,点上传按钮会唤起系统窗口,Selenium 无法直接触达系统窗口按钮。处理思路有两条,我按优先级排列。

第一步先尝试一个反直觉但好用的方法:点开上传组件后,直接对已聚焦的元素发送路径。其实不管上传控件是不是 input 标签,很多组件在弹窗打开前会生成一个隐藏的 file input 用于接收文件选择结果,你只要能把路径送进去就行。做法是找页面上所有 input type=file 的节点,直接用 send_keys 硬塞路径:

inputs = driver.find_elements(By.CSS_SELECTOR, "input[type='file']") for input_el in inputs: try: input_el.send_keys("C:/tmp/upload_file.txt") break except Exception: continue

这个方法能解决相当一部分假上传框的兼容问题。不要小看它,我帮同事排查上传用例时,十次里拿这个方案解决了至少一半。

4.3 方案三:Windows 弹窗兜底,用外部工具接管

如果页面连隐藏 input 都没有,上传是纯 Flash 或纯 JS 组件,点击后弹出的是系统的“文件选择”对话框,那就绕不开系统级操作了。最常用的方案是 Python 的 pywinauto(Windows)或者 pyautogui 直接模拟键盘输入路径。

这里给一个在 Windows 下用 pywinauto 的标准流程:

# 点击上传按钮触发系统对话框 driver.find_element(By.ID, "upload-btn").click() # 切换并控制系统窗口 from pywinauto import Desktop # 等待对话框出现后,往文件名输入框里输入路径,再点打开 try: dlg = Desktop(backend="uia").window(title="打开") dlg.wait("visible", timeout=10) dlg.Edit.set_text("D:\\reports\\upload.xlsx") dlg.Button.click() except Exception: # 回退方案:直接 pyautogui 输入 import pyautogui pyautogui.sleep(1) pyautogui.typewrite("D:\\reports\\upload.xlsx", interval=0.05) pyautogui.press("enter")

需要特别提醒:pyautogui 是全局键盘鼠标模拟,执行时不要动鼠标键盘,否则会串场。这个方案我通常放在最后,因为它对执行环境要求高、可维护性差,脚本换台机器可能就因为输入法或分辨率不一样而失败。有条件的话优先引导前端改造上传组件,或者走接口层测试来覆盖文件上传场景。

还有一个纯前端的技巧:拖拽上传场景可以尝试直接用 JS 构造 DataTransfer 对象,把 File 放进输入框的 files 属性。这个方案对前端容器要求较高,我见过有人用得很溜,但从零写起来代码量大,而且未来维护成本不小,这里就不展开了。

5. 窗口、iframe 与 JS 执行:解决顽固问题的三把钥匙

前四章覆盖了“找元素-做操作-等就绪-传文件”的主链路。但实际项目中,Selenium 脚本的大量翻车现场发生在两个特殊容器里:多窗口和多层框架。外加一个 JavaScript 执行能力,它可以解决一些光靠 UI 操作解决不了的问题。我把这三个主题放在同一章,因为它们的共性都是“跨越 Selenium 默认操作边界”。

5.1 多窗口切换:别再用 driver.close 当“万能钥匙”

页面点击一个链接后新开 tab 或新开窗口,这种场景在 OAuth 登录、第三方支付模拟、详情页预览里很常见。不处理窗口切换的话,你会发现不管怎么定位,元素就是找不到——因为 driver 还停留在旧窗口上下文里。

标准流程是三步:先记录当前窗口句柄,再触发新窗口打开,最后遍历句柄切换到目标窗口。

# 1. 记录当前窗口句柄 original_window = driver.current_window_handle # 2. 点击触发新开窗口的元素 driver.find_element(By.LINK_TEXT, "前往支付平台").click() # 3. 等待新窗口句柄出现 WebDriverWait(driver, 10).until( lambda d: len(d.window_handles) > 1 ) # 4. 切换窗口 new_window = [w for w in driver.window_handles if w != original_window][0] driver.switch_to.window(new_window)

注意:切换窗口以后,原来的元素变量全部失效,需要重新定位。还有,部分环境下浏览器会直接复用已有 tab(target=_self),根本没有新句柄出现,这种情况就不会走进这个分支。

还有个小技巧:页面标题更能代表目标窗口身份。如果开了两个新窗口,它们的句柄顺序不稳定,那可以用 title 判断:

for handle in driver.window_handles: driver.switch_to.window(handle) if "订单详情" in driver.title: break

5.2 iframe 切换:定位不到元素的第一嫌疑

iframe 嵌套是自动化脚本的头号杀手,比定位不懂 XPath 更常见。任何 find_element 找不到节点的时候,我都建议先看这个元素在不在 iframe 里。判断方式:在浏览器开发者工具里看元素样式,如果是嵌入在 iframe 文档下的,直接查源码外层有没有<iframe>标签。

处理方案就是切换上下文:

# 切换到 iframe:可以传 index、name/id 或定位元组 driver.switch_to.frame(driver.find_element(By.CSS_SELECTOR, "iframe[src^='https://pay']")) # 或直接指定 index driver.switch_to.frame(0) # 操作完回到默认内容 driver.switch_to.default_content()

如果 iframe 里还嵌着 iframe(三层嵌套),需要一层层切进去,操作完再一层层切回来。有一种快速刺穿全部 iframe 的方法——但这个依赖元素就在某一个 iframe 里,写起来较复杂,很少用。日常就是逐层两步走:

driver.switch_to.frame("outer") driver.switch_to.frame("inner") # 此时可以定位内层元素 ... # 退回外层 driver.switch_to.parent_frame()

switch_to.parent_frame() 是切回上一层 iframe,而 switch_to.default_content() 是直接回到最外层文档,两者用途不同,别混了。

Shadow DOM 的处理是另一个故事。新版 Chrome 的 Selenium 已经支持穿透 shadow-root 定位,但写法稍不一样——用 driver.find_element 配合 shadow root 的向下查找。大部分情况下,遇到 web component 封装的内部元素,最好先找业务方确认是否暴露>element = driver.find_element(By.ID, "submit-btn") driver.execute_script("arguments[0].click();", element)

页面滚动也可以交给 JS,比 ActionChains 的 scroll_to_element 稳定:

driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # 滚到具体元素位置 driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element)

去掉输入框的 readonly 属性、修改元素的禁用状态、设置日期控件的值,都是 JS 大显身手的地方。举一个很常见的高频场景:日期选择插件遮挡点不动,直接通过 JS 赋值并触发 change 事件:

date_input = driver.find_element(By.ID, "date") driver.execute_script( "arguments[0].value = '2024-03-18'; arguments[0].dispatchEvent(new Event('change', {bubbles: true}));", date_input, )

JS 执行还经常用来取数据。比如你想验证页面某个接口返回的数据在 DOM 里有没有正确渲染,可以:

result = driver.execute_script( "return document.querySelector('.total-amount').innerText;" ) assert "188.00" in result

需要注意的是:execute_script 的参数注入必须写成 arguments[0] 这种形式,不要直接拼接字符串,否则容易踩转义和引号嵌套的问题,而且可能引发注入风险。返回结果只有三种类型——直接量、WebElement 引用、数组/对象——传出来的字典结构要小心处理。

5.4 截图与下载:测试报告的硬依赖

自动化脚本跑完总得有凭据。save_screenshot 是保存当前窗口截图,element.screenshot 是截某个元素。常用写法:

driver.save_screenshot("reports/screenshots/failed_case.png") # 或者只截元素 element.screenshot("reports/screenshots/avatar.png")

Allure 报告集成时,把截图以字节流形式加到报告里是常规操作。实际项目中截图是定位问题的重要素材,在断言失败或者异常捕获里自动截图,能省下大量排查时间。

try: wait_click(driver, By.ID, "save-btn") except Exception: driver.save_screenshot("reports/error_" + datetime.now().strftime("%Y%m%d_%H%M%S") + ".png") raise

下载文件这块,Selenium 4 提供了 set_download_path 设置默认下载目录,配合 ChromeOptions 可处理下载弹窗和文件名变化。下载完成后用 os.path.exists 或 glob 来判断文件是否落地,再配合文件大小不为0来确认下载完整性。

prefs = {"download.default_directory": "/tmp/downloads"} options = webdriver.ChromeOptions() options.add_experimental_option("prefs", prefs) driver = webdriver.Chrome(options=options)

6. 高频报错排查:把这些异常背下来,项目成功一半

最后这部分用速查表收尾,把自动化测试里最常见的异常、原因和解决路径写成一张可以直接翻的表格。这份清单是我在实际项目和带团队过程中一点点积累的,基本覆盖 90% 以上报错。

异常常见原因处置方向
NoSuchElementException元素不在 DOM、定位表达式错误、元素在 iframe 或 Shadow DOM 中先切 iframe/shadow root,再确认定位表达式
StaleElementReferenceException页面刷新或元素被重新渲染重新查找元素,或改用 WebDriverWait + locator 模式
ElementClickInterceptedException元素被其他元素遮挡用 JS click、滚动到可见位置、或者先关闭遮挡元素
ElementNotInteractableException元素存在但不可见/不可操作检查是否被 CSS 隐藏、是否在视口外、是否 readonly
TimeoutException显式等待超时条件一直未满足确认等待的条件写对没、元素是否在 iframe 里
InvalidSelectorException定位表达式语法错误用浏览器的 console 验证 XPath/CSS 表达式
WebDriverException驱动与浏览器版本不匹配、端口占用重新匹配 chromedriver 版本、重启相关进程
SessionNotCreatedException浏览器崩溃或初始化失败清理浏览器缓存/配置,检查 options 是否有冲突

把表格里的每一项都当成排查手册来用,效率会提升很多。后面补充几个大家特别容易反复踩的经验细节。

第一个,浏览器版本和驱动版本必须严格匹配。Chrome 浏览器一升级,驱动器就得跟着换,这属于周期性踩坑。建议把 chromedriver 的版本管理写成自动脚本,定时去检查匹配版本,必要时直接用 webdriver-manager 这类包自动管理。

第二个,失败重试和用例隔离。脚本里捕获异常后简单重试一次,可以挡掉 80% 的偶发性失败;但重试逻辑要设计好,不能无限重试掩埋真实问题。用例之间还要注意状态隔离,不要上一用例登录了,下一用例还在登录态里跑,互相影响非常难排查。每个用例的 tearDown 里做数据清理或者回滚操作,是测试设计的基本功。

第三个,环境差异。同一套脚本,本地 Chrome、CI 容器里的无头模式、远程 node 机器,行为有时候不一样。一个典型的坑:无头模式(headless)下元素尺寸和行为与有头模式不同,需要特别关注元素可见性和下载行为。我通常在 CI 环境专门做一次“无头模式下的元素兼容确认”,避免脚本只在本地通过。

还有个工具层面的补充:Selenium Manager 从 4.6 开始内建了自动驱动管理,大部分情况下初始化 driver 时不用手动下载 chromedriver,它会根据当前浏览器版本自动匹配。对于刚入门的人,这个能省掉很多配置烦恼。但要注意,内网环境或者集中执行的测试机可能需要手动指定 driver 路径,这一点在团队环境里经常被忽略。

7. 最后分享一个实战小技巧:用封装代替散装代码

从定位、交互、等待、上传到窗口和 iframe,单个函数都知道怎么用了,最后一步是把它们串成可复用的“页面操作库”。我见过太多测试工程源码:每个用例里地方都长得差不多,但都是复制粘贴的五行六行。建议的做法是给项目建一个 base_page 类,把高频操作做成方法,这样你的用例代码会很短、报错定位也极快。

class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def find(self, by, value): return self.wait.until(EC.presence_of_element_located((by, value))) def click(self, by, value): self.wait.until(EC.element_to_be_clickable((by, value))).click() def input_text(self, by, value, text): element = self.find(by, value) element.clear() element.send_keys(text) def switch_frame(self, locator): self.wait.until(EC.frame_to_be_available_and_switch_to_it(locator)) def get_text(self, by, value): return self.find(by, value).text

具体业务页面继承这个基类,把页面元素定位放到类属性里,用例直接调方法,代码结构一下就清晰了。这算不上什么高深设计,但它是从“能跑的脚本”走向“扛得住回归的自动化项目”之间很关键的一步。

再补一个建议:日常写脚本时多留意那些稳定不变的元素属性(data-testid、固定 class 前缀、id 命名规范),这些往往是前端工程规范化的产物,比随便写 XPath 可靠得多。跟开发团队约定好 ui 自动化测试专用标识,很多稳定性问题能直接根治。

文件上传、窗口切换、iframe、JS 兜底、显式等待封装,这套组合拳打下来,市面上绝大多数 web 自动化需求都够用了。剩下那些罕见特例,等真遇到了,再拿起这篇里的思路去扩展也不迟。

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

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

立即咨询