ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Selenium自动化测试实战:从环境搭建到核心函数与工程化封装

Selenium自动化测试实战:从环境搭建到核心函数与工程化封装 最近在帮团队搭一套 Web 自动化测试的底子技术选型绕来绕去最后还是回到 Selenium 上。说实话这玩意儿我前前后后用了四五年从早期 Firefox 时代一路用到今天 Chrome 119每次有新人加入第一周基本都在跟浏览器驱动、元素定位、等待时机这些东西死磕。标题里写的是“selenium使用包括函数部分”听着挺朴素但恰恰是这些最基础的东西最值得沉下心来捋一遍。因为实际项目里跑得不稳、动不动就报 NoSuchElementException 的九成问题都出在函数用得不透、时序没控制好。这篇文章就当作一次完整的实操笔记从环境准备讲到核心函数再聊到工程化封装最后把排错经验一并列出来。Selenium 本身能做什么一句话总结用代码驱动真实浏览器模拟人的点击、输入、滚动、切页等操作再对页面结果做校验。它适合谁不管你是刚接触自动化测试的 QA还是想写爬虫又不想被反爬绕晕的开发甚至只是想把重复性的网页操作脚本化的运维同学这篇文章都值得看完。内容偏实战我自己踩过的坑都会标出来能帮你少走不少弯路。1. Selenium环境搭建与驱动匹配1.1 安装selenium库的正确姿势现在安装 Selenium 已经比前几年省心太多了直接用 pip 装就行pip install selenium装的时候建议顺手把版本固定住。我个人习惯用pip install selenium4.27.0这种写法或者装完立刻执行pip freeze requirements.txt导出依赖。原因很简单Selenium 4.x 的 API 比 3.x 改动不小尤其元素等待、下拉框处理这些地方团队里版本不统一代码互相跑不通光填坑就能耗掉大半天。实测下来 Selenium 4.6 之后的版本自带驱动管理能力不再需要手动去下载 chromedriver 落地到指定路径这对新手来说真的是救命改进。检查安装是否成功在 Python 交互环境里跑一句from selenium import webdriver print(webdriver.__version__)能正常输出版本号就没问题。如果这里报 ModuleNotFoundError先确认你是不是装进了当前激活的虚拟环境。很多新人折腾半天最后发现 pip 和 python 根本不是同一套环境。1.2 浏览器驱动下载与配置Selenium 本身不包含浏览器它只是通过 WebDriver 协议去指挥浏览器干活。所以你还得有一个驱动相当于浏览器给外部程序留的“遥控接口”。我用得最多的是 Chrome 配 chromedriver。从 4.6 版本开始Selenium 提供了一个特别顺手的工具类SeleniumManager代码里不再需要手动指定 executable_pathfrom selenium import webdriver driver webdriver.Chrome()只要 Chrome 浏览器本身是正常安装的Selenium 会自动去匹配对应版本的驱动下载后缓存在本地。这背后做的事情说穿了就是去检查浏览器版本然后找到匹配的驱动二进制。但有一点要提醒如果公司内网环境访问不了外网Selenium Manager 自动下载驱动会卡住这时候只能手动到驱动仓库下载对应版本放到系统 PATH 里。版本匹配的原则记住一条chromedriver 的大版本号和 Chrome 浏览器的大版本号必须一致。比如你本地 Chrome 是 120.0.6099那驱动也必须找 120 开头的。版本不匹配时启动浏览器会直接抛 SessionNotCreatedException报错信息里会明明白白写着版本不兼容。1.3 环境变量问题排查标题的热搜词里有一串“无法将 xxx 项识别为 cmdlet、函数、脚本文件或可运行程序的名称”这几乎是所有命令行工具的通用坑Selenium 也不例外。最常见的是 pip 命令找不到pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称原因通常有两个一是你的 Python 安装时没勾选“Add Python to PATH”二是你当前激活的虚拟环境里压根没装 pip。前者去系统环境变量里把 Python 的 Scripts 目录加进去就行后者直接在命令行里跑python -m pip install selenium用模块方式调用 pip可以绕开 PATH 问题。chromedriver 如果提示“不是内部或外部命令”直接把驱动所在目录加到 PATH或者干脆把驱动文件放到 Python 安装目录的根下一劳永逸。2. 核心函数体系拆解从定位到动作2.1 最常用的定位函数find_element 全家桶Selenium 里所有操作的前置条件都是“找到元素”。定位函数用不好后面全是空中楼阁。Selenium 4 提供了两种写法一种是老牌的driver.find_element_by_id(username)另一种是 Selenium 4 推荐的统一入口from selenium.webdriver.common.by import By driver.find_element(By.ID, username)我个人只用第二种。第一种在老版本被标记为废弃后虽然短期内还能用但每次跑都会刷 deprecation 警告日志干净度很影响排查效率。统一入口的好处还在于你只需要记一个函数名配合不同定位策略切来切去定位策略By 参数适用场景IDBy.ID元素有唯一 id优先级最高类名By.CLASS_NAME有明确 class且不与其他元素重复名称By.NAME表单控件常用 name 属性XPathBy.XPATH结构复杂时兜底方案灵活但慢CSS 选择器By.CSS_SELECTOR比 XPath 快写法简洁链接文本By.LINK_TEXT精确定位超链接部分链接文本By.PARTIAL_LINK_TEXT只记得链接部分文字时用实际项目里的选型优先级我一般按 ID CSS_SELECTOR XPath 来。ID 是最稳定的锚点但很多前端框架生成的 ID 里带动态随机串每次刷新都变。遇到这种CSS 选择器拿属性值定位反而更稳# 定位一个>element driver.find_element(By.ID, username) element.clear() # 清空输入框 element.send_keys(admin) # 输入内容 element.click() # 点击 print(element.text) # 获取可见文本 print(element.get_attribute(class)) # 获取任意属性这些函数看起来简单细节里全是坑。先说 clear很多人在输入前不清空结果文本框里残留上次的值拼出来的字符串就错了。尤其浏览器会自动填充账号密码时send_keys 之前不 clear 一下最终值会是“自动填充的值你输入的值”。再说 click直觉上就是“点一下”但实际执行时会要求这个元素是可见的、且宽高都不为 0。如果一个元素在页面底部被折叠起来了直接 click 会报 ElementNotInteractableException。这时候系统性地用 scroll_into_view 先滚过去from selenium.webdriver.common.action_chains import ActionChains ActionChains(driver).scroll_to_element(element).perform() element.click()send_keys 还有一批隐藏功能比如操作键盘按键。上传文件时也能用 send_keys 直接传文件路径但 Chrome 出于安全限制只能操作 input[typefile] 元素不能直接发路径到任意输入框# 回车键 element.send_keys(Keys.ENTER) # 组合键 CtrlA element.send_keys(Keys.CONTROL, a) # 上传文件 driver.find_element(By.CSS_SELECTOR, input[typefile]).send_keys(/path/to/file.pdf)这里有个很大的易错点文件上传对话框是操作系统原生窗口Selenium 默认没有能力操作它。但如果你是真实的上传场景直接把文件路径通过 send_keys 喂给 input[file] 就能绕过弹窗这是做自动化时最常用的招。2.3 浏览器控制函数get、back、forward、refresh 与窗口管理和元素操作并列的是一批驱动对象级函数用于控制浏览器整体行为driver.get(https://example.com) # 打开页面 driver.back() # 后退 driver.forward() # 前进 driver.refresh() # 刷新 driver.title # 获取页面标题 driver.current_url # 获取当前URL driver.maximize_window() # 最大化窗口 driver.set_window_size(1920, 1080) # 设置窗口尺寸 driver.get_screenshot_as_file(shot.png) # 截图特别提一下 get_screenshot_as_file这个函数在排查问题时候就是救命稻草。我做的项目里凡是跟第三方支付页面对接回调不稳定的时候全靠失败那一刻的截图还原现场。更讲究一点可以把截图命名为“时间戳用例名”存到指定目录万一 CI 失败直接看图片定位问题比看一堆堆的异常栈快得多。窗口管理也值得单独说说。很多业务点完按钮会新开 TabSelenium 默认还停留在旧页面上这时候你必须手动切窗口# 获取当前所有窗口句柄 handles driver.window_handles # 切到最后一个新窗口 driver.switch_to.window(handles[-1])有同事问过为什么写了 click 之后明明页面已经跳了find_element 还是找不到八成就是没切窗口或者没做等待。窗口切换和元素可见性是两个独立的维度都要处理。3. 进阶函数从硬等待到智能等待3.1 等待函数与时机的博弈刚接触 Selenium 的朋友最常见的错误就是“洒水式”的硬等待import time time.sleep(5)不是说不能用而是不能到处用。硬等待会让整个用例的执行时间被拉长好几倍100 个用例每个多 sleep 3 秒就是 300 秒的损耗。更麻烦的是网络状况好的时候用不着等那么久网络状况差的时候 5 秒又不够用例开始变得“时而绿、时而红”。正确的思路是显式等待。Selenium 提供了一对黄金搭档WebDriverWait 和 expected_conditions通常别名成 ECfrom selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10, poll_frequency0.5) login_btn wait.until( EC.element_to_be_clickable((By.ID, login-btn)) ) login_btn.click()这段代码的意思是最多等 10 秒每 0.5 秒去检查一次按钮是否可点击一旦可点击立刻返回元素超过 10 秒就抛 TimeoutException。相比 sleep它把“等待”逻辑从固定时长变成了“轮询直到条件满足”这才是自动化的正确姿势。EC 模块里常用的条件函数我列几个EC 函数作用EC.presence_of_element_located元素出现在 DOM 中但不一定可见EC.visibility_of_element_located元素可见EC.element_to_be_clickable元素可见且可点击EC.text_to_be_present_in_element元素文本包含指定内容EC.title_contains页面标题包含关键字EC.alert_is_present弹窗出现我自己总结了一个经验规则元素定位用 duration 多等一点没事操作类的等待点击、输入后触发跳转重点在“可交互”这一层而不是“出现”这一层。DOM 里出现不代表能点击可能还在加载样式、事件还没绑定完。所以在实际项目里 EC.element_to_be_clickable 是我用得最多的条件比单独 presence 稳定不少。3.2 下拉框处理的Select函数热搜词里专门有 select 函数这是 Selenium 里一个特别容易踩坑的点。很多人拿到下拉框下意识想点击结果发现原生select元素点了没反应因为它的展开选项根本不是普通的 DOM 元素是浏览器原生绘制的东西。Selenium 为原生 select 提供了专门封装from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.ID, city)) # 按可见文本选择 select.select_by_visible_text(北京) # 按 value 属性选择 select.select_by_value(110000) # 按索引选择从0开始 select.select_by_index(1)三个函数的区别很简单页面给用户看的是“北京”HTML 里 value 可能是“110000”索引就是第几个 option。实际项目里优先用 select_by_visible_text因为它在页面上的最终呈现结果最直观别人 review 代码的时候一眼能懂。但要注意Select 类只对原生的select标签有效。现在很多前端框架比如 Vue Element UI、React Ant Design自作主张做了自定义下拉框本质是 input 下拉列表的组合这时候你用 Select 函数会直接报 UnexpectedTagNameException。处理这种自定义下拉思路就要变先点击触发下拉列表展开再根据选项文本定位弹层中的具体项driver.find_element(By.CSS_SELECTOR, .ant-select-selector).click() wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, .ant-select-dropdown))) driver.find_element(By.XPATH, //div[contains(class,ant-select-item) and text()上海]).click()这里有个细节自定义下拉的选项可能很多但只有当前渲染出来的部分能定位。如果列表很长选项是滚动懒加载的还得先在弹层里执行滚动再继续找后面的选项。3.3 鼠标键盘操作的ActionChains函数有些操作已经超出了简单的 click 和 send_keys比如鼠标右键、双击、悬停、拖拽、按住不放。Selenium 的 ActionChains 就是把一连串动作组织成链条最后统一 performfrom selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys actions ActionChains(driver) # 悬停到菜单项上 hover driver.find_element(By.ID, nav-products) actions.move_to_element(hover).perform() # 双击 actions.double_click(element).perform() # 右键 actions.context_click(element).perform() # 拖拽 actions.drag_and_drop(source, target).perform()ActionChains 最典型的业务场景是电商后台的拖拽排序把某个商品从左边的可选列表拖到右边的已选列表。以前有人用模拟键盘 Tab方向键去操作稳定性极差。换成 drag_and_drop 之后整个用例的执行时间从 20 秒降到了 8 秒。一个细节之坑perform 之前 ActionChains 维护的动作链表是累加的。如果你在一个 actions 对象上先 move_to_element 又 double_click这两个动作会被一起执行。所以每次动作串联完必须执行 perform 结束链条否则下次写动作会把上次的残留一并带上。习惯上我每次都会新建一个 ActionChains 对象不复用。3.4 页面JavaScript注入execute_script再核心的自动化也少不了执行 JavaScript 的场景。页面上的滚动、属性修改、隐藏元素操作很多时候 Selenium 原生函数做不到但一段 JS 搞定# 滚动到页面底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) # 给元素加一个红色边框方便肉眼核对 driver.execute_script(arguments[0].style.border2px solid red, element) # 去掉 readonly 属性让原本禁止填写的日期框可输入 driver.execute_script(arguments[0].removeAttribute(readonly), element)arguments[0] 是 Selenium 注入 JS 时的参数占位符后面的 Python 参数会按顺序映射到 arguments[0]、arguments[1]... 这个方法在绕过只读控件时几乎是唯一解。我以前遇到过业务里明明有“申请日期”字段但因为权限控制被设成 readonly手动测试能通过自动化却一直输不进去。开发说“你们测试不能动这个字段”我说“行那就验证一下只读逻辑”然后改用 JS 去调用 removeAttribute顺利解决了。execute_script 的返回值也很有用。如果 JS 最后 return 了一个值Python 这边能直接拿到# 获取页面可见高度 viewport_height driver.execute_script(return window.innerHeight;) # 判断页面是否滚动到底部 is_bottom driver.execute_script(return window.scrollY window.innerHeight document.body.scrollHeight;)这给了我一个思路不用总依赖 Selenium 的 API 去拿页面状态有时候直接用 JS 问浏览器“你现在的滚动位置是多少”比任何封装都准确。4. 工程化封装中的函数设计4.1 用 Page Object 模式封装页面函数学会 Selenium 函数只是第一层。真正考验功力的是把散落各处的函数组织成一套可维护的工程结构。Page Object页面对象模式是目前业界最经典的方案核心思想每个页面封装成一个类页面上要操作的元素定位和动作统统收敛到类方法里测试用例只跟“页面对象”交互不直接碰 driver。举个例子登录页可以设计成class LoginPage: def __init__(self, driver): self.driver driver self.username_input (By.ID, username) self.password_input (By.ID, password) self.login_button (By.ID, login-btn) def input_username(self, username): self.driver.find_element(*self.username_input).send_keys(username) def input_password(self, password): self.driver.find_element(*self.password_input).send_keys(password) def click_login(self): self.driver.find_element(*self.login_button).click()看着好像只是把定位符集中存放了但实际好处太多了。第一前端改版把登录按钮的 id 换了你只需要改这一处所有用例集体生效。第二测试用例读起来是业务语言而不是 Selenium 语法login_page LoginPage(driver) login_page.input_username(tester01) login_page.input_password(password123) login_page.click_login()这比我最早写的“find_element_by_id 一万行”清晰太多了。代码 review 的时候别人不需要知道 Selenium 有哪些函数只需要看方法名就知道当前用例在做什么。我还会在基类里封装一些公共操作比如统一等待、统一截图class BasePage: def __init__(self, driver): self.driver driver self.wait WebDriverWait(driver, 10) def find(self, locator): return self.wait.until(EC.visibility_of_element_located(locator)) def click(self, locator): self.find(locator).click()这样所有页面类继承 BasePage就自动获得“可见即所得”的查找能力不用每个方法都重复写 wait.until。4.2 通用函数的三个高阶技巧重试、截图、日志工程化到后半程你会发现自己重复写“失败重试”“失败截图”“记录日志”这三件事。把它们抽成通用函数整个框架的稳定性会明显上升。失败重试的通用逻辑可以封装成装饰器比如点击确认弹窗后页面可能短暂闪烁第一次点击没生效重试一次就好了import functools def retry_on_failure(max_retries3): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception: if attempt max_retries - 1: raise time.sleep(1) return wrapper return decorator需要注意重试不是万能药。如果元素找不到是因为前端压根改版了重试只会浪费 3 秒再报错。所以我的做法是只在“偶发性、时序性”问题上用重试定位符变更、断言失败这类确定性错误直接抛出来让人去查。失败截图我喜欢做成 pytest 的钩子。每一条用例失败的时候自动截图文件名带用例名和时间戳写进 allure 报告。这样失败之后根本不用翻日志直接打开报告看图就知道浏览器当时的状态。5. 常见问题与排查技巧实录5.1 元素找不到先分三路排查NoSuchElementException 是 Selenium 里出现频率最高的报错没有之一。接手过太多人问“为什么我明明复制了 XPath 还是找不到”这里把排查思路整理成一张速查表类别现象排查方向时序问题报错时间很快页面其实还没加载完加显式等待窗口问题元素在 iframe 或新窗口里先 switch_to 再定位定位符问题元素确实在 DOM但定位表达式写错了用浏览器 console 验证 XPath时序问题是最常见的。页面用了异步加载元素在 DOM 里“迟到”了你写代码时看到的页面是加载完的状态但脚本执行到它的那一瞬间还没有。处理方式参考 3.1 的显式等待。窗口和 iframe 的问题常常被忽略。很多管理系统后台、第三方登录组件、广告弹层都是 iframe 内嵌的你在父页面 find_element 自然找不到。先切进去driver.switch_to.frame(frame_name) # 按 name 切 driver.switch_to.frame(0) # 按索引切 driver.switch_to.frame(frame_element) # 或直接传我们找到的元素切换完之后别忘了用完再切回默认内容driver.switch_to.default_content()这个坑我踩过不止一次切进 iframe 操作完成之后没有切回主文档结果下一步找主页面元素一直报错排查半天才发现自己在 iframe 的上下文里出不来。5.2 元素被遮挡与点击偏移还有一个高频问题元素在页面上的位置被其他元素盖住了比如弹窗遮罩、banner 悬浮层、Cookie 协议条。Selenium 定位到了元素但点击时浏览器提示“元素不可交互”。最直接的解决办法是手动把遮挡层干掉或者把元素滚动到可见区域element driver.find_element(By.ID, submit) driver.execute_script(arguments[0].scrollIntoView({block: center});, element) element.click()如果页面挂了遮挡层可以用 JS 把遮挡元素隐藏driver.execute_script(document.querySelector(.modal-mask).style.displaynone;)注意这种绕过手段只适合测试环境。如果线上页面被拦截说明业务有合规要求必须先确认能不能跳过别为了一时方便把功能测出一个假通过。拦截点想到这我还想分享一个更隐蔽的固定头部导航栏横向占满整个页面页面上方的按钮虽然出现在视口里但被导航栏盖住了实际可点击区域。这时 scrollIntoView 加上 block: center 之后依然点不中因为点击坐标落在遮罩层上。我的处理办法是滚动多一点driver.execute_script(window.scrollBy(0, -120);)手动把页面再往上挪 120 像素让目标元素避开导航栏遮挡区域。5.3 弹窗处理与多窗口切换弹窗分成两类浏览器原生 alert和页面自定义弹层。两者处理逻辑完全不同。原生 alert 是浏览器级别的没法用 find_element 定位必须用 switch_to# 等待弹窗出现 wait.until(EC.alert_is_present()) alert driver.switch_to.alert print(alert.text) # 获取弹窗文本 alert.accept() # 点击确定 # alert.dismiss() # 点击取消自定义弹层本质是一个 div左上角那个关闭按钮就是普通元素。一定要注意弹层出现有没有过渡动画。有些团队给弹层加了 0.3s 的 fade 过渡脚本执行太快在动画期间点关闭按钮会 miss就等着报错吧。这种只能等着最稳妥的方式还是 EC.element_to_be_clickable 等它完全出现。多窗口切换在 2.3 里写过基础用法这里补一个封装好的切换函数def switch_to_new_window(driver, old_handles): 切到新打开的窗口old_handles 是切换前的窗口句柄列表 wait WebDriverWait(driver, 10) wait.until(lambda d: len(d.window_handles) len(old_handles)) new_handles driver.window_handles for handle in new_handles: if handle not in old_handles: driver.switch_to.window(handle) break这里面最核心的一步是用 lambda 条件等待新窗口生成。如果窗口都没打开你就急着切拿到的是一个不存在的句柄后面全崩。5.4 浏览器驱动闪退与浏览器崩溃最后聊聊一个玄学问题脚本跑到一半浏览器窗口突然全关了报错信息还指向 chromedriver。这种情况在 Linux 服务器上跑无人值守的任务时尤其常见。第一确认系统内存是否吃紧。浏览器是内存大户一个 Chrome 实例轻松吃掉 500MB 以上服务器上同时跑多个用例时 OOM 是常事。排查方法很简单盯一下系统监控里的内存曲线或者执行 free -h 看剩余内存。第二检查浏览器版本是否被自动更新了。Chrome 默认开启自动更新今天跑得好好的明天驱动对不上直接崩。生产环境建议固定浏览器版本或者写脚本定期检查版本一致性。第三无头模式headless下更容易暴露资源问题。用--headlessnew跑无头模式时页面加载但肉眼不可见很多人以为更省资源其实不然。我给团队的建议是调试时一定用有头模式能看见浏览器行为只在 CI 上跑批量回归时切换到无头模式且加上窗口尺寸设置from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) options.add_argument(--window-size1920,1080) options.add_argument(--disable-gpu) driver webdriver.Chrome(optionsoptions)记得设置 window-size否则无头模式下页面视口默认可能只有 800x600很多响应式布局的页面在这个尺寸下的 DOM 结构和 1920 宽下完全不同直接导致元素定位失败。6. 我个人在实际项目里的体会Selenium 这套工具链说简单也简单无非就是定位元素、执行操作、验证结果说复杂也复杂真实业务里的动态加载、跨域跳转、弹窗嵌套、浏览器兼容每一样都够折腾一阵。但在我看来掌握 Selenium 的核心不在于记住多少个 API 函数而在于建立起“情况驱动选型”的判断力什么时候用普通定位、什么时候必须显式等待、什么时候切 iframe、什么时候用 JS 兜底。我见过太多人把 Selenium 用成了“录制回放”工具脚本拿到手能跑就万事大吉等前端一改版就全部报废。真正健壮的自动化测试靠的是把函数用得恰到好处把等待、重试、截图这些防御性措施做足。前面分享的每一条经验都是我实打实踩过坑换来的结论。如果你照着这篇里的思路走一遍再去看你们项目的自动化脚本多半能发现不少可以优化的地方。最后再分享一个习惯每次在 Selenium 里遇到新问题我都会先写一个最小复现脚本让问题在最短代码路径里暴露出来。这个习惯帮我最快找到问题的根源也让我能快速验证某个函数在当前浏览器版本下的真实行为。Selenium 版本迭代快、浏览器版本差异大靠记忆推断远不如亲手验证靠谱。希望这篇文章能帮你少走几步弯路有不确定的地方动手跑一跑就知道了。
返回列表