尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Selenium Web自动化测试入门:元素定位、等待与Page Object实战

Selenium Web自动化测试入门:元素定位、等待与Page Object实战 做了这么多年 Web 自动化每次有人问我自动化测试入门该学什么我第一反应都是先学 Selenium。这个答案听着可能不够时髦毕竟现在 Playwright、Cypress 天天吹自己是新王但 Selenium 在 Web 自动化这个领域里的位置依然稳得像压舱石尤其是配合 Python 一起用的时候。Selenium 说白了就是一套操作真实浏览器的工具库它能让你用代码启动 Chrome、Firefox、Edge 这些主流浏览器模拟用户在页面上的真实操作——点击按钮、填写表单、滚动页面、切换标签页、上传文件——然后把页面里的文字、元素状态、弹窗信息抓回来做校验。它解决的核心问题是把重复枯燥的回归测试交给机器去跑人只负责看结果、分析问题。这篇内容不打算堆概念就围绕 Selenium 基础说清楚三件事它是什么、怎么开始用、踩坑点在哪。适合完全没接触过自动化的测试新手也适合已经会写简单脚本、但经常被元素定位和等待问题劝退的同学。我尽量用平时跟同事交流的口吻来讲该给代码给代码该讲原理讲原理争取你看完就能上手。1. 先搞清楚Selenium 到底在解决什么问题1.1 Web 自动化的本质把重复劳动交给脚本做测试的同学都懂手工回归测试最磨人的不是测出 bug而是同一套用例在发版前连续点几十遍。我以前待过一个项目每次发版前光走核心业务流程的冒烟测试就要大半天点得人手酸。Web 自动化测试干的事就是把这一套打开页面 → 填数据 → 点提交 → 看结果的动作写进脚本让浏览器自己半夜里跑第二天早上看报告就行。这里要强调一个认知自动化测试不是录一遍脚本然后重放而是用代码控制浏览器并且有能力对运行中的状态做判断。市面上录制回放的工具很多能录出脚本但录不出稳定的判断逻辑。Selenium 之所以是首选是因为它提供的是 WebDriver 这套统一的标准协议通过浏览器原生的驱动ChromeDriver、GeckoDriver 这些去跟浏览器通信相当于给浏览器装了一个编程入口。正因为是标准协议你用 Python 写的脚本换一台装 Java 环境的机器也能跑换浏览器本质上也是一套思路。1.2 Selenium 家族三件套WebDriver、IDE、Grid新手经常被网上怎么安装 Selenium 插件这种问题搞晕。市面上确实有一个叫 Selenium IDE 的浏览器扩展能录制脚本但说句实话它在真实项目里基本是个玩具顶多拿来快速做一个原型验证证明这个网站可以用自动化操作。真正干活的是另外两样东西。WebDriver核心库用代码控制浏览器日常说的写 Selenium 脚本写的都是 WebDriver。Selenium Grid分布式执行组件可以让你在多台机器、多种浏览器组合上并行跑用例适合回归量大、浏览器兼容矩阵复杂的团队。初学者第一阶段用不上但知道有这个东西就行。现在要学就直接学 Selenium 4别回头翻老文档了。Selenium 4 全面支持 W3C 标准协议API 也比 3 代规整得多还内置了相对定位器Relative Locator、无头模式这些能力写代码的体验舒服不少。1.3 为什么选 Selenium 而不是 Playwright/Cypress既然提到了新工具我索性把选型这事讲明白。Selenium 的优势是生态沉淀了十几年遇到任何问题几乎都能搜到答案而且支持 Java、Python、C#、JavaScript、Ruby 这些主流语言。企业里老项目基本都是 Selenium你进了团队不用重新学一套。Playwright 和 Cypress 确实是后来者尤其在靠浏览器调试协议直连浏览器这条技术路线上跑得更快等待机制也更聪明。但它们有一些使用前提Cypress 主要支持 JavaScript/TypeScript而且只跑在自己管理的浏览器实例里有些场景会有局限Playwright 对 Python 和 Java 的支持也不错未来值得关注。我的建议很简单如果是个人想入行 Web 自动化先把 Selenium 吃透因为它的知识结构能帮你理解浏览器自动化底层的通用原理等基础扎实了再结合团队技术栈去研究新工具切换成本非常低。2. 环境准备从 pip 安装到跑通第一个脚本2.1 安装 Selenium 库和插件误区先把最容易绕晕的事说清楚Selenium 不是浏览器插件它是一个 Python 库。你不需要去浏览器里安装 Selenium而是在 Python 环境里执行一条命令pip install selenium装完验证一下版本import selenium print(selenium.__version__)我习惯在真实项目里用 venv 建一个虚拟环境避免把系统 Python 搞乱。这里顺带说一句网上所谓的怎么安装 Selenium 插件大多数情况问的是 Selenium IDE 那个浏览器扩展但它对于日常自动化测试来说不是必需品别被带偏。真正要在项目里用的是selenium这个 Python 包加上一个浏览器驱动。2.2 浏览器驱动决定脚本死活的关键Selenium 控制浏览器靠的不是魔法而是每个浏览器厂商提供的 Driver。Chrome 对应 ChromeDriverFirefox 对应 GeckoDriverEdge 对应 EdgeDriver。驱动的作用是翻译官把 Selenium 的代码指令翻译成浏览器能听懂的原生命令。装完 Selenium 库只是第一步没有对应驱动脚本一启动就会直接报SessionNotCreatedException。这里有个大坑驱动版本必须和浏览器版本匹配。比如你的 Chrome 升级到了 131ChromeDriver 还是 126大概率启动就挂。匹配规则看大版本对应最稳妥的办法是去对应驱动的官方下载页找到和你浏览器主版本号一致的驱动下载后放到项目目录或系统 PATH 里。Selenium 4.6 之后内置了 Selenium Manager它会自动帮你查找并下载合适的驱动不用再手动折腾。但我还是建议每个人手动配一遍驱动因为公司 CI 环境里手动管理驱动仍然是基本功。Windows 下最简单的方式是下载一个chromedriver.exe放到项目根目录然后在代码里这样指定from selenium import webdriver driver webdriver.Chrome(executable_path./chromedriver)2.3 第一个最小脚本环境通没通一跑便知环境配好后写一个尽量小的脚本验证一下from selenium import webdriver driver webdriver.Chrome() driver.get(https://www.saucedemo.com/) print(driver.title) driver.quit()跑起来以后你会看到一个 Chrome 窗口自动打开、跳到指定页面、打印页面标题、然后关闭。看到这个现象说明 Web 自动化环境已经通了。有几点细节值得注意driver.quit()会关闭所有窗口并退出驱动进程driver.close()只关当前标签页。我建议脚本结束一定用quit()否则后台可能残留 chrome driver 进程长期跑任务时特别吃内存。webdriver.Chrome()如果报错说找不到 ChromeDriver大概率是驱动没放在 PATH 里或者 Selenium Manager 没法自动找到匹配版本。如果不想弹出浏览器窗口可以在开头加上options.add_argument(--headlessnew)让浏览器在无头模式下运行适合放在服务器上跑定时任务。3. 元素定位自动化的地基3.1 八大定位方式速查自动化脚本写得好不好一半看定位一半看等待。Selenium 里定位元素就是通过元素的属性、文本、层级关系把页面上某个元素找出来。Selenium 提供 8 种基本定位方式id、name、class name、tag name、link text、partial link text、xpath、css selector。国内很多现代前端框架React、Vue生成的元素 id 经常带随机串所以id不一定总能用这时候要结合前端规范来选。实际工作里被用得最多的还是 CSS Selector 和 XPath 两种剩下的方式不是没用而是适用面偏窄。比如link text只对a标签的纯文本有效tag name定得太泛很容易命中一堆兄弟元素。3.2 CSS Selector 和 XPath到底该学哪个我的判断是能用 CSS Selector 就用 CSS写不了的场景再用 XPath。CSS Selector 语法简洁、可读性好、性能也好配合 id 和 class 定位非常顺。比如# 通过 id driver.find_element(By.CSS_SELECTOR, #login-username) # 通过 class driver.find_element(By.CSS_SELECTOR, .btn-primary) # 通过属性 driver.find_element(By.CSS_SELECTOR, input[namepassword])XPath 的优势在两点一是支持通过文本内容定位页面里常见点击确定这种场景用 XPath 的//button[text()确定]最直接二是支持轴定位比如某个节点前一个兄弟节点父节点的父节点这种层级跨越用 XPath 的ancestor、following-sibling写起来很省事。CSS Selector 在这些场景基本无能为力。举一个真实场景一个列表页面每行有个编辑按钮需要点第 3 行的编辑。如果行数据没有唯一属性可以用行元素的索引结合 XPathrows driver.find_elements(By.CSS_SELECTOR, table tbody tr) target_row rows[2] target_row.find_element(By.CSS_SELECTOR, .edit-btn).click()这里注意find_elements返回的是列表下标从 0 开始所以我这里取的是第 3 行。这个写法在表格类页面里非常常用但在动态分页或前端虚拟列表下不适用那就要考虑数据本身来判断哪行而不是死记索引。3.3 等待机制脚本稳不稳全靠它现代 Web 页面基本都是异步加载的请求发出去之后接口数据没返回时元素可能根本不在 DOM 里。如果脚本里直接find_element大概率抛出NoSuchElementException或者TimeoutException。所以写 Selenium 必须懂等待。Selenium 等待有三种流派强制等待time.sleep(3)固定睡几秒。简单粗暴但慢且脆页面快的时候傻等页面慢的时候又不够用。隐式等待driver.implicitly_wait(10)设置一个全局超时时间在查找元素时如果元素没出现会在超时时间内反复尝试。这个好用但有不少坑它只对元素在不在生效判断不了元素是否可见、是否可点击而且设了全局值后有些显式等待的时间语义会受影响。显式等待WebDriverWait配合expected_conditions这是我最推荐的方式精确控制某个条件满足后再往下走。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) wait.until(EC.presence_of_element_located((By.ID, login-username))) wait.until(EC.element_to_be_clickable((By.ID, login-submit)))presence_of_element_located表示元素出现在 DOM 中但可能不可见element_to_be_clickable表示元素可见且可点击这两个条件在复杂页面上差别很大。我的习惯是凡是后面要点、填、选的元素等待条件一律用element_to_be_clickable只有纯读取文字时才用visibility_of_element_located这类条件。3.4 页面元素枚举与仅存储定位元数据这点特别重要也是我想重点分享的。很多同学第一次学 Selenium 喜欢这么做submit_btn driver.find_element(By.ID, submit) submit_btn.click()代码本身没有错但在复杂用例里如果过早把 WebElement 对象缓存到变量里页面一旦发生重绘、跳转、局部刷新之前拿到的元素对象就失效了再操作它大概率抛StaleElementReferenceException。这个异常翻译过来就是元素过期了。我推崇的做法用一个词概括就是仅存储定位元数据。所谓定位元数据指的是定位方式 定位值这一对信息而不是定位出来的元素对象。用 Python 实现的话可以用类常量把定位元数据集中管理from selenium.webdriver.common.by import By class LoginPageLocators: USERNAME_INPUT (By.ID, username) PASSWORD_INPUT (By.NAME, password) LOGIN_BUTTON (By.CSS_SELECTOR, button[typesubmit])然后在使用时每次都现场重新查找wait.until(EC.element_to_be_clickable(LoginPageLocators.LOGIN_BUTTON)).click()这样做有几个好处一是在同一个类里集中维护定位信息页面改版时只需要改一处二是每次都重新查找天然规避了StaleElementReferenceException三是定位字符串可读性很好代码评审时一眼能看出来这一步在操作什么元素。更进一步如果你用 Java 或 C# 这种强类型语言可以考虑把定位信息真的放进枚举里。Python 里没有天然的枚举类型但用类常量加 NamedTuple 也能达到类似效果。这其实就是 Page Object 设计模式的一部分思路把页面元素和操作逻辑分离元素定位就是元数据业务方法才负责动作。这套思路在项目里用起来之后脚本维护成本能下降一大截。4. 动手实操写一个完整的登录流程脚本4.1 场景设计一个用例怎么从需求到落地学 Selenium 最忌讳跑通一个打开百度的脚本就觉得会了那只是验证环境。真正考验人的是写一个能稳定跑、能告诉你页面正不正常的用例。我拿 Sauce Demo 这个专门用来练手的电商网站做演示场景很简单打开登录页输入用户名和密码点登录等待进入商品列表页然后校验页面上出现了预期的商品元素。这个场景覆盖了 Selenium 最核心的几个动作打开页面、输入框操作、点击按钮、等待元素、断言校验。练熟这一套基本能覆盖 80% 的基础用例。4.2 完整代码和逐行解释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 # 配置浏览器 options webdriver.ChromeOptions() options.add_argument(--start-maximized) driver webdriver.Chrome(optionsoptions) wait WebDriverWait(driver, 10) try: # 1. 打开登录页 driver.get(https://www.saucedemo.com/) # 2. 等待并输入用户名 wait.until(EC.element_to_be_clickable((By.ID, user-name))).send_keys(standard_user) # 3. 输入密码 password_input driver.find_element(By.ID, password) password_input.send_keys(secret_sauce) # 4. 点击登录 login_button driver.find_element(By.ID, login-button) login_button.click() # 5. 等待商品列表出现断言当前页面标题 wait.until(EC.visibility_of_element_located((By.CLASS_NAME, inventory_list))) assert Swag Labs in driver.title print(登录成功进入商品页) finally: driver.quit()逐行说几个关键地方。第 5 步的assert看起来有点不像预期中的断言——但它确实是一个原生判断如果条件不成立会抛AssertionError在测试框架里正好算失败用例。这里的断言要选登录成功后必然会出现的标志性内容我用的是商品列表容器元素这比断言页面 URL 更稳因为很多单页应用 URL 刷新后不变化。另外注意第 2 步我把等待元素可点击和输入内容写在了一行。这种链式写法在元素明确会出现时很简洁但如果元素不稳定我还是建议拆分方便定位到底是哪一步超时。4.3 失败截图和页面快照自动化报告的地基脚本跑挂了如果只是红字报错排查效率极低。我通常会在except块里加一个截图逻辑遇到异常就把现场留成 png 文件import time from selenium import webdriver driver webdriver.Chrome() try: driver.get(https://www.saucedemo.com/) # 模拟一个会失败的查找 driver.find_element(By.ID, not-exist) except Exception: timestamp time.strftime(%Y%m%d_%H%M%S) driver.save_screenshot(ferror_{timestamp}.png) raise finally: driver.quit()这种做法在自研框架里或者配合 pytest 的失败钩子都能直接用。你还可以把当前页面 DOM 用driver.page_source保存下来用来分析动态渲染的元素。截图加 HTML 快照基本能覆盖 90% 的定位和渲染问题。4.4 用 Page Object 把脚本变成能维护的框架上面那个脚本是功能正确但结构粗糙的过程化写法。真实项目里页面很多、用例很多如果每个用例都直接把元素定位和业务操作写在一起一个页面改版几十个用例跟着改非常痛苦。所以我强烈建议从第一天就养成 Page Object 的习惯。核心思想就一句每个页面对应一个类页面里的元素定位信息是这个类的属性元数据用户对这个页面能做的操作是这个类的方法。测试用例只调用方法不直接碰元素定位。用刚才的登录页面改一下class LoginPage: USERNAME_INPUT (By.ID, user-name) PASSWORD_INPUT (By.ID, password) LOGIN_BUTTON (By.ID, login-button) def __init__(self, driver, wait): self.driver driver self.wait wait def open(self): self.driver.get(https://www.saucedemo.com/) def login(self, username, password): self.wait.until(EC.element_to_be_clickable(self.USERNAME_INPUT)).send_keys(username) self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password) self.driver.find_element(*self.LOGIN_BUTTON).click()这里有个 Python 语法细节find_element(*self.PASSWORD_INPUT)。因为PASSWORD_INPUT的值是(By.ID, password)这样一个元组加星号是把元组展开成两个位置参数等价于find_element(By.ID, password)。这种写法配合仅存储定位元数据的原则非常优雅我在项目里一直这么用。Page Object 不要滥用嵌套。我见过有人把所有元素全塞进一个超级页面类里结果类膨胀到上千行反而降低了可维护性。正确的粒度是一个用户视角的页面模块一个类比如登录页归登录页商品列表归商品列表。跨页面跳转的用例在测试代码里串动作就行。5. 常见问题与排查技巧实录5.1 元素定位不到先按这四步查NoSuchElementException和TimeoutException是最常见的两个异常。遇到定位不到我一般按这个顺序排查检查元素是否在 iframe 里。如果目标元素嵌在 iframe 里你必须先切换进去driver.switch_to.frame(frame_name)操作完再switch_to.default_content()切回来。检查页面是否由 JavaScript 动态渲染。数据请求没完成或者列表是懒加载需要先滚动到底部再定位。检查元素属性是否动态变化。ID 里有时间戳、随机数的改用 CSS 属性或 XPath 定位。检查是否弹出了新的窗口或遮罩层。如果被遮罩挡住find_element能定位到但点击会报ElementNotInteractableException。这套排查顺序基本能解决 80% 的找不到元素问题。剩下的 20%通常是业务逻辑里有什么前置状态没满足比如按钮在某个条件下才显示那就要回到业务逻辑上看。5.2 StaleElementReferenceException缓存元素的锅这个异常前面提过我现在讲讲完整的排查思路。它的触发机制是元素对象还在但它之前所在的那个 DOM 节点已经不存在了。典型场景是翻页、刷新、改变筛选条件。解决办法就是别缓存 WebElement 变量每次都重新查找或者在操作前用WebDriverWait重新等待条件满足。一个我用得比较多的写法是如果碰到那种可能刷新也可能不刷新的页面就用一个等待函数包住目标操作配合重试机制最多试两三遍。虽然有人觉得这是脏手段但它比在线上环境排查偶发问题要实用得多。这类问题的核心其实是提醒你把元素定位信息当作快照还是路标来用。Selenium 的 WebElement 本质是个动态查询只有每次实时查找才能保证拿到的永远是最新状态。5.3 驱动版本不匹配怎么处理启动时报SessionNotCreatedException十有八九是 ChromeDriver 和 Chrome 版本对不上。处理方式分几步查看本机浏览器版本打开 Chrome地址栏输入chrome://version看主版本号。去 ChromeDriver 官方下载页找到对应主版本号的驱动。把下载的驱动放到一个目录把它加进 PATH或者在webdriver.Chrome()里通过executable_path参数指定路径。Selenium 4.6 有 Selenium Manager 会自动处理依赖我建议把它当省事方案但在服务器上跑自动化任务时还是明确指定executable_path更可控。版本不匹配这个问题最容易出现在浏览器自动升级了但 CI 里的驱动没跟着升级这种场景所以团队里最好固定浏览器版本或者加一条检查脚本。5.4 iframe、弹窗、窗口句柄三个老顽固刚做自动化的人最容易卡在这三个地方iframe定位不到元素时优先怀疑它。切进去用switch_to.frame切出来用switch_to.default_content。注意如果 iframe 是动态加载的切换前要先用等待。alert 弹窗原生浏览器的 alert、confirm、promptSelenium 处理起来很简单但很容易忘driver.switch_to.alert.accept()或dismiss()。不过现在前端框架里大多数弹窗都是 DOM 元素做的假弹窗那不是 alert而是普通元素用常规定位就行。多窗口点击链接打开了新标签页需要用window_handles切换。标准做法是记下当前窗口句柄点击后等新窗口出现再switch_to.window(新句柄)。这三个场景我建议每个都单独写个小 demo 跑一遍比看十篇文档都管用。它们的特点是必须做上下文切换不做切换元素就是找不到、弹窗就是关不掉。5.5 排查速查表异常 / 现象常见原因优先处理方式NoSuchElementException元素未渲染、定位方式不对、在 iframe 中加显式等待、检查 iframe、换定位方式TimeoutException元素始终未满足预期条件确认页面状态、等待条件是否太严、元素是否被拦截StaleElementReferenceException操作过期元素对象避免缓存 WebElement每次重新定位ElementNotInteractableException元素被遮挡、只读、透明等待可点击、用 JS 滚动元素进视野SessionNotCreatedException驱动和浏览器版本不匹配换匹配版本驱动ElementClickInterceptedException弹窗遮罩盖住了元素先关闭弹窗再点击这张表是我自己实践里最常遇到的六个问题。实际项目里肯定还有更复杂的但你把这几类处理方式练熟至少能解决 80% 的日常报错。写到这里Selenium 的基础算是完整过了一遍。我在实际用 Selenium 的过程里最大的体会是工具本身不难难的从来都是稳定的等待策略和清晰的元素管理方式。很多项目自动化做不下去不是脚本跑不通而是脚本脆得不行一跑就挂、一挂就没人维护。如果你看完这篇能真正意识到别缓存元素、用显式等待、把定位元数据集中管理这三件事的分量那这篇基础介绍的价值就远比你复制一段登录脚本要大。也建议你拿 Sauce Demo 这类练手站点自己把 Page Object 模式完整跑一遍比看任何教程都管用。下次再聊的话可以深入讲讲 pytest 集成、数据驱动、测试报告这些偏框架层面的东西。
返回列表