做了这么多年 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 对应 ChromeDriver,Firefox 对应 GeckoDriver,Edge 对应 EdgeDriver。驱动的作用是翻译官,把 Selenium 的代码指令翻译成浏览器能听懂的原生命令。装完 Selenium 库只是第一步,没有对应驱动,脚本一启动就会直接报SessionNotCreatedException。
这里有个大坑:驱动版本必须和浏览器版本匹配。比如你的 Chrome 升级到了 131,ChromeDriver 还是 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("--headless=new"),让浏览器在无头模式下运行,适合放在服务器上跑定时任务。
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[name='password']")XPath 的优势在两点:一是支持通过文本内容定位,页面里常见"点击确定"这种场景,用 XPath 的//button[text()="确定"]最直接;二是支持轴定位,比如"某个节点前一个兄弟节点""父节点的父节点"这种层级跨越,用 XPath 的ancestor、following-sibling写起来很省事。CSS Selector 在这些场景基本无能为力。
举一个真实场景:一个列表页面,每行有个"编辑"按钮,需要点"第 3 行的编辑"。如果行数据没有唯一属性,可以用行元素的索引结合 XPath:
rows = 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[type='submit']")然后在使用时,每次都现场重新查找:
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(options=options) 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(f"error_{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、prompt,Selenium 处理起来很简单但很容易忘:
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 集成、数据驱动、测试报告这些偏框架层面的东西。