Selenium 在自动化测试里的地位有点像建筑行业里的水泥——看着不起眼但几乎所有人入门都绕不开它。它本质上是一套浏览器自动化工具通过代码驱动真实浏览器去完成点击、输入、滚动、切换页面这些操作再把结果交给断言去校验。它解决的核心问题很朴素把重复、耗时、容易出错的回归测试变成机器自动执行的任务让测试人员从机械点击里解放出来。如果你是想转自动化测试的测试工程师、想补测试能力的开发同学或者是刚接触软件测试的在校生从 Selenium 入门几乎是成本最低、资料最全的一条路。这篇内容我会从环境搭建一路讲到 pytest 框架和报告生成中途会把元素定位、等待策略、常见坑位整个过一遍。1. 自动化测试的整体设计思路动手之前先盘一盘1.1 为什么新手都从 Selenium 入手很多刚接触自动化测试的人会纠结“怎么选工具”。市面上能听到的选项不少Appium 主打移动端自动化Playwright 是微软出品的下一代浏览器自动化方案TSMaster 则偏向汽车总线与硬件测试跟网页 UI 自动化关系不大。可一旦打开招聘软件看岗位要求Selenium 依然是出现频率最高的关键词没有之一。这里有个容易被忽略的点Selenium 是开源项目背后有庞大的社区和十几年的真实项目沉淀。你踩过的坑大概率早就有人踩过并留下了解决方案。这种“老”反而是新手的红利。相比之下Playwright 虽然在 API 设计和稳定性上有优势但很多公司内部遗留的自动化资产、脚本、框架底座都是基于 Selenium 的入职后你还是要先接住这些存量代码。先学 Selenium再去看 Playwright通路是顺畅的反过来一上来就只玩新工具进公司面对老框架反而容易懵。从学习曲线来说Selenium 的命令风格很直白找到元素、操作元素、校验结果。三个动作循环往复覆盖了绝大多数 UI 自动化场景。下面这张表可以帮你快速建立工具认知工具定位主要场景上手难度SeleniumWeb UI 自动化浏览器端回归测试、冒烟测试、兼容性测试低Appium移动端自动化Android / iOS App 的 UI 测试中PlaywrightWeb UI 自动化需要多浏览器并发、无头浏览器场景低TSMaster汽车总线测试车载通信、硬件仿真与诊断高1.2 自动化测试能解决什么不能解决什么有人会把自动化测试想得很神觉得有了脚本就能替代所有手工测试。实际上它最擅长的领域是回归测试和冒烟测试版本频繁迭代时把已经稳定的功能脚本化每次发版前自动跑一遍快速确认核心链路没被改坏。它也能干一些人工不爱干的重复活比如批量导入数据后校验界面展示、多浏览器下截图比对、接口返回与页面显示的一致性检查。但自动化测试解决不了探索性测试的问题。新功能怎么做用户才顺手、界面配色是否合理、交互是否符合直觉这些需要人去看、去试、去判断。还有一类场景自动化做起来性价比极低需求每周大变、页面结构天天调整脚本改动成本高过手工执行成本这种情况就别硬上自动化。把自动化测试理解成“把人的判断力留在测试设计阶段把重复执行交给机器”定位就准确了。2. 环境准备搭建一套能跑通的 Selenium 开发环境2.1 Python、虚拟环境与依赖安装Selenium 支持 Java、Python、C#、Ruby 多种语言但国内测试圈最主流的是 Python主要原因是语法简单、第三方库丰富写自动化脚本效率高。环境准备第一步就是装 Python。建议直接装 3.8 以上版本到官网下载安装包时记得勾选“Add Python to PATH”省得后面命令行找不到 python。装完 Python我强烈建议你先建一个虚拟环境不要一股脑把包装到全局。虚拟环境隔离项目依赖换个项目也不会污染全局包这是自动化测试工程化的第一步。在项目根目录执行这三条命令就能完成初始化python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install selenium这里有个实际经验很多同学卡在安装成功但运行报 ModuleNotFoundError十有八九是 IDE 里选的解释器不是当前虚拟环境。在 PyCharm 里要手动选择 venv 下的 Python 路径在 VSCode 里则要点击右下角解释器重新选择。2.2 浏览器驱动最容易踩的版本坑Selenium 本身只负责发指令真正把指令翻译成浏览器动作的是一个叫 WebDriver 的程序。Chrome 驱动就叫 chromedriverFirefox 驱动叫 geckodriverEdge 驱动叫 msedgedriver。浏览器驱动必须和浏览器版本对应如果对不上运行时会直接报类似 SessionNotCreatedException 的错误提示版本不匹配。手动下载驱动需要先去 Chrome 地址栏输入 chrome://version 看版本号再去驱动下载站找对应版本然后放到 Python 路径或指定路径。这套流程第一遍能跑通但每到浏览器自动升级之后就会再次踩坑。更省心的方式是用 webdriver-manager 这个库自动匹配版本pip install webdriver-manager有了它脚本里就不用再操心驱动路径了。下面这段代码就是完整的环境验证from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome(serviceService(ChromeDriverManager().install())) driver.get(https://www.baidu.com) print(driver.title) driver.quit()第一次运行会自动下载对应版本的 chromedriver之后每次启动先检查本地版本不匹配就自动更新。这一步能帮你省掉后续非常多的环境问题。2.3 第一个脚本打开浏览器并输出页面标题上面这段代码看起来简单但它已经把 Selenium 的核心流程带出来了创建 WebDriver 实例、发起访问、读取信息、退出浏览器。其中最关键的是最后一行 driver.quit()很多新手会随手写成 driver.close()。close 只关闭当前标签页quit 才会结束整个浏览器进程。如果用 closeWindows 上经常会看到 chrome.exe 进程残留跑几次测试电脑就卡得不行。读取页面信息时driver.title 可以直接拿到浏览器标签页上的标题driver.current_url 可以拿到当前地址这两项常常被用来做最基础的断言。第一个脚本跑通之后你可以再试着加一句 WebDriverWait 等待页面加载完成但这部分我放到后面专门讲。先确保脚本能稳定打开浏览器、拿到结果、正常退出就算跨过第一关了。3. 元素定位让脚本精准找到页面控件3.1 八大定位方式各怀绝技自动化测试所有操作的前提是先找到元素。Selenium 提供了八种定位方式分别是 id、name、class name、tag name、link text、partial link text、xpath、css selector。它们的适用场景差异很大整理成表会看得更清楚定位方式用法示例适用场景注意点idfind_element(By.ID, login)元素有唯一的 id 属性首选定位最稳定namefind_element(By.NAME, username)表单控件有 name 属性页面中可能重复class namefind_element(By.CLASS_NAME, btn)按 CSS 类名定位类名可能有多个取一个即可tag namefind_element(By.TAG_NAME, input)批量处理同类型标签命中范围过大link textfind_element(By.LINK_TEXT, 登录)精确定位超链接文本只能用于 a 标签partial link textfind_element(By.PARTIAL_LINK_TEXT, 登)部分匹配超链接可能匹配到多个xpathfind_element(By.XPATH, //input[idlogin])综合条件定位可层级搜索语法稍复杂但最灵活css selectorfind_element(By.CSS_SELECTOR, #login)用 CSS 语法定位性能好但无法按文本定位我的建议是优先级从高到低id 能用就不用别的id 没有再看 name 或 class都没有最后上 xpath。id 在同一个页面里理论上唯一定位失败率最低。class name 很可能一个元素同时有多个类名你只需要传其中一个就行不用全写。3.2 XPath 和 CSS 选择器怎么选XPath 是几乎所有自动化测试人员都要掌握的技能因为总会有元素既没有 id 也没有 name。它分为绝对路径和相对路径两种写法。绝对路径从 html 根节点开始逐层往下写页面结构稍微调整就会失效日常工作中基本只用相对路径。下面这几个 XPath 写法覆盖了高频场景# 按属性精确匹配 driver.find_element(By.XPATH, //input[idkw]) # 模糊匹配属性值 driver.find_element(By.XPATH, //div[contains(class, search)]) # 文本内容匹配 driver.find_element(By.XPATH, //button[text()登录]) # 多个条件组合 driver.find_element(By.XPATH, //input[nameusername and typetext]) # 按位置索引 driver.find_element(By.XPATH, (//div[classitem])[2])CSS Selector 对比 XPath 的优势是写法更简洁、执行性能更好。比如 id 定位在 CSS 里写成 #loginclass 定位写成 .btn属性定位写成 [nameusername]。但 CSS 有一个天然缺陷它不能按元素文本内容定位。所以遇到“必须根据页面上显示的文字找按钮”的场景还是得回头用 XPath。我自己写定位时有个习惯先去浏览器开发者工具里右键元素 Copy Copy XPath拿回来作为参考再手动改成相对路径版本。直接用浏览器生成的绝对路径虽然能跑但维护成本很高一次前端改版就可能整条脚本报废。3.3 定位不到元素的排查顺序自动化测试最经典的报错就是 NoSuchElementException碰到这个先别急着改代码按顺序排查往往半小时内能定位。首先是看元素是不是在 iframe 里iframe 相当于页面中嵌套的另一个文档Selenium 默认只操作主文档必须先用 driver.switch_to.frame() 切进去才能看到里面的元素。然后是看元素是否是动态加载的。很多页面采用异步渲染脚本执行到 find_element 时元素还没出现这时候正确的做法不是把定位语句复制三遍而是用显式等待等它出现。再往下就要检查你的定位表达式是否命中了多个元素find_element 只返回第一个如果页面有几个相同类名的元素你操作的很可能不是目标那个。还有一个容易被忽略的点元素可能在 DOM 中存在但不可见比如被遮罩层挡住、处于折叠状态、或者设置了 display:none。判断一个元素是否真正可操作不仅要看它是否存在还要看它是否可见、是否可点击这部分就是下一章等待策略要解决的问题。4. 浏览器操作与等待策略把稳定性刻进 DNA4.1 高频操作浏览器级与元素级Selenium 里元素操作没有太多玄机核心就几个方法click 点击、send_keys 输入、clear 清空、text 获取文本、is_selected 判断选中状态。真正需要留个心的是浏览器级别的操作包括页面跳转、窗口尺寸控制、截图以及 Cookie 处理这些方法在稳定性排查和问题定位时特别有用。# 浏览器级操作 driver.get(https://www.example.com) # 打开页面 driver.refresh() # 刷新页面 driver.back() # 后退 driver.forward() # 前进 driver.maximize_window() # 最大化窗口 driver.set_window_size(1366, 768) # 指定窗口尺寸 driver.get_screenshot_as_file(page.png) # 截图保存 driver.get_cookies() # 查看当前页面的 Cookie # 元素级操作 element driver.find_element(By.ID, kw) element.clear() element.send_keys(Selenium 入门) element.click() print(element.text)截图是一个非常实用的调试手段。脚本执行到某一步失败时把当时页面截图和当前 URL 记录到日志里能省下大量“靠猜”的时间。很多团队会把失败截图自动挂到测试报告上后面对接 Allure 时可以一起实现。4.2 三类等待强制、隐式、显式的本质页面加载慢是自动化测试不稳定的最大来源。早期很多人图省事直接在代码里写 time.sleep(3)这就是强制等待。它的逻辑很简单不管页面加载完了没有先睡满三秒再说。问题在于网络快的时候白白浪费三秒网络慢的时候三秒又不够脚本越跑越脆。隐式等待是对 driver 的全局设置设置之后每个 find_element 都会在元素没找到时进行轮询查找直到超过设定时间才抛出异常。典型写法是 driver.implicitly_wait(10)。它虽然好用但只针对“元素存在”不管元素是否可见、是否可点击。如果按钮被遮罩挡住隐式等待等到天亮也不会帮你处理。显式等待则是针对特定条件的等待最常用的是 WebDriverWait 配合 expected_conditions。它每隔一段时间去检查一次指定条件满足后立即返回超时才抛出 TimeoutException。这才是解决“动态渲染元素”的正解下面的代码示意了三种等待的核心用法import time from selenium import webdriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By driver webdriver.Chrome() # 强制等待不推荐在生产脚本中使用 time.sleep(3) # 隐式等待全局轮询查找元素存在 driver.implicitly_wait(10) # 显式等待等待元素可点击 element WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, submit)) ) element.click()4.3 我的等待实践尽量不写裸 sleep写自动化脚本久了我的等待策略基本就一句话能用显式等待解决就不用隐式等待能不用强制等待就不用。如果页面首次打开时加载很慢可以在入口处加一个 WebDriverWait 等待某个页面特征元素出现比如等待登录框可输入再去处理后续逻辑。显式等待里最常用的条件有这么几个presence_of_element_located 等待元素出现在 DOM 中visibility_of_element_located 等待元素可见element_to_be_clickable 等待元素可点击staleness_of 等待元素从 DOM 中移除常用于判断页面是否已经开始跳转。还有一个小经验不要在网页一打开时就立刻点击也不要盲目把等待时间设成 60 秒。等待时间设太长失败用例的执行时间会被拉得很夸张。一般 10 到 15 秒够用特殊情况再单独调。5. 用 pytest 加 Selenium 搭起一套可维护的框架5.1 pytest 是自动化测试的骨架单个 Selenium 脚本只能做验证要把它变成一套可持续运行的测试体系需要引入测试框架。pytest 是 Python 生态里最主流的测试框架它没有复杂的配置写起来非常顺。它提供给自动化的核心价值有三个用例组织、断言报告、脚手架扩展。pytest 的用例识别规则很简单文件名以 test_ 开头或者以test.py 结尾文件内的测试函数以 test开头。一个最简单的测试长这样def test_login_page_title(): assert 登录 in 测试登录页面执行 pytest 后它会自动搜索当前目录下符合条件的测试文件并运行。针对自动化测试我们会在这个基础上扩展 fixture、参数化、Allure 报告下面逐块讲。5.2 fixture 管理浏览器实例的优雅玩法fixture 是 pytest 对测试前置和后置逻辑的封装。比如每个测试用例前创建浏览器、用例结束后关闭浏览器这个动作就可以定义成一个 fixture。它最大的优势是能设置作用域让多个用例复用同一个浏览器实例避免每个用例都重新启动浏览器导致的时间浪费。import pytest from selenium import webdriver pytest.fixture(scopeclass) def driver(): driver webdriver.Chrome() yield driver driver.quit() class TestSearch: def test_search(self, driver): driver.get(https://www.baidu.com) assert 百度 in driver.titlescopeclass 表示同一个测试类下的所有测试方法共享这一个 driver 实例测试类执行完才执行 driver.quit()。如果场景需要每个用例独立浏览器就把 scope 改成 function。不用 fixture 时很多同学会把 driver 定义在全局模块里写起来简单但用例之间互相影响很难排查fixture 可以保证每次测试的生命周期是受控的。5.3 参数化驱动测试数据自动化测试经常需要覆盖多组数据比如不同搜索关键词、不同登录账号、不同金额边界值。pytest 的参数化装饰器能把你从复制多条用例中解放出来。import pytest pytest.mark.parametrize(keyword,expect, [ (Selenium, Selenium), (pytest, pytest), (自动化测试, 自动化测试), ]) def test_browser_search(driver, keyword, expect): driver.get(https://www.baidu.com) search_box driver.find_element(By.ID, kw) search_box.send_keys(keyword) search_box.submit() assert expect in driver.page_source运行 pytest 时这个函数会被展开成三条独立用例参数一和参数二分别传入。将来要加数据只需要往列表里追加元组执行自动生成新的用例。参数化让测试数据和测试逻辑分离代码维护成本直线下降。5.4 集成 Allure 报告让结果一目了然测试跑完没有报告等于白跑Allure 是目前最主流的自动化测试报告方案。安装依赖后就两步先指定结果存放目录再生成 HTML 报告。pip install allure-pytest pytest --alluredir./allure-results allure generate ./allure-results -o ./allure-report --clean allure open ./allure-reportAllure 报告最大的价值在于展示层级清晰环境信息、用例步骤、失败日志、截图都能挂到对应用例里。把上一章提到的截图逻辑封装进 fixture 的钩子函数中失败时自动截图并挂到报告上排查问题会非常高效。很多公司面试时问自动化测试框架搭建基本就是这个套路pytest 管理用例、Selenium 执行操作、Allure 输出报告、再搭一层 Jenkins 做定时构建。6. 常见问题排查与防坑手册6.1 元素找不到NoSuchElementException 的应对清单这个异常是新手遇得最多的。除了前面说的 iframe 和动态加载原因还有一个高频坑是大小写和空格。HTML 属性值是区分大小写的classsearch-btn 不能写成 Search-Btn。另外属性前后可能有空格比如 classbtn primary定位时只能用其中一个类名不能直接复制整个 class 字符串去匹配。还有一种情况是元素在页面加载过程中被短暂替换。比如旧按钮先出现框架渲染后替换成新按钮脚本在替换瞬间抓到了旧按钮点击时元素已经分离。这种问题表面上是 NoSuchElementException实际是时序问题需要用等待机制解决。6.2 元素找到了但点击无效元素存在且可见但点击后没有任何反应这种“假成功”比直接报错更头疼。常见原因是元素上面覆盖了一个透明遮罩层尤其在弹窗、浮层出现时最容易发生。Selenium 的点击坐标落在元素中心点如果遮罩层盖住了这个点点击事件就被层级上方的元素消费掉了。碰到这种情况可以先尝试用 WebDriver.execute_script 让目标元素滚动到可视区域element driver.find_element(By.ID, submit) driver.execute_script(arguments[0].scrollIntoView();, element) element.click()如果还是不行用 JavaScript 直接触发点击事件也能绕过遮罩问题但这属于非常规手段能解决业务里特殊交互的卡点不要滥用。6.3 iframe 和多窗口切换iframe 是自动化测试里一个大家族式的坑。页面里嵌了 iframe直接 find_element 永远是找不到的必须先切进去。切换的基础写法是 driver.switch_to.frame(frame_id)切换回主文档用 driver.switch_to.default_content()。多层嵌套 iframe 需要逐层切换顺序搞反就报错。多窗口切换则是另一类高频问题。点击链接打开新标签页后driver 还停留在旧页面这时需要拿到所有窗口句柄再切换driver.window_handles 返回当前所有窗口的句柄列表新窗口在最末尾切换过去后如果还想回到主窗口用第一个句柄再切回来。这个流程在登录过程经常会遇到因为很多第三方登录会弹出新窗口。6.4 登录态与验证码问题自动化测试最棘手的问题之一就是验证码。现在很多系统的登录流程都接入了图形验证码或滑块验证这类验证码识别成本高也不稳定。工程上的常规做法是绕过验证码本身测试环境关闭验证码功能或者由开发提供万能验证码或者通过调用接口提前拿到登录态再用 Cookie 写入浏览器实现免登录。# 提前从接口获取登录后的 Cookie 写入当前浏览器 driver.get(https://www.example.com) driver.add_cookie({name: session_id, value: 真实接口返回的cookie值}) driver.refresh()如果公司测试环境不允许关闭验证码那就在自动化脚本里尽量复用已有会话的登录态别让每条测试用例都走一遍完整的登录流程。6.5 脚本不稳定与并发执行脚本今天能过明天挂了在 UI 自动化里太常见。跑挂之后先别急着改代码按顺序排查是不是页面响应慢导致等待超时是不是测试数据被前一次执行改掉了是不是用例之间有先后依赖。测试用例之间最好是完全独立的每条用例都能单独跑不要依赖上一条用例的中间状态。并发执行可以用 pytest-xdist 插件但它要求浏览器实例彼此隔离。每个进程用独立 driver 实例不要共享同一个全局 driver。并发虽然能缩短执行时间但对流量和设备要求高小团队可以先别碰先把单机稳定性跑出来再考虑并发。写到最后分享一点我的体会我用 Selenium 做过从零到一的框架也维护过几百条用例最大的感受是自动化测试的难点从来不是写脚本而是控制不确定性。环境变了、页面变了、数据变了脚本就跟着闹脾气。与其追求用一个巨复杂的框架把所有情况都兜住不如先把定位、等待、容错这些基本功磨扎实。学 Selenium 不要只看教程一定要亲手把环境、脚本、报告这串链路全部点亮一遍。点亮之后你会发现什么 pytest、Allure、接口测试其实都是在同一个底层逻辑上生长出来的。后续想深入可以沿着数据驱动、关键字驱动、Selenium Grid 分布式执行这几条路继续走但先把今天这些地基打稳才是最重要的。