ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python+Selenium自动化测试入门:从环境搭建到编写稳定用例

Python+Selenium自动化测试入门:从环境搭建到编写稳定用例 身边不少朋友在学自动化测试时都有过类似的困惑打开招聘网站的岗位要求发现十有八九都带一句“熟悉 Selenium”看社区里的教程又发现一堆名词像 WebDriver、元素定位、Page Object 往脸上砸还没开始写代码就已经被劝退。我自己的经验是Selenium 本质上就是一个“浏览器遥控器”你只要弄清楚了它怎么干活写第一个脚本其实就是几分钟的事。这篇文章要聊的就是怎么用 Python 把 Selenium 这块敲门砖快速拿起来从环境准备一路走到能独立写用例。这套组合特别适合这几类人刚转行做测试、需要回归重复功能的前端开发、想用代码自动处理网页操作的后台工程师还有自己搭了个小项目想顺手把冒烟测试做掉的人。它不需要你会什么很深的编程基础会 Python 的基础语法就够用。我会按自己实际动手的路径来写不是把官方文档翻译一遍而是在每一步上都告诉你“为什么这么做”和“哪些地方容易翻车”。1. 为什么是 Selenium在动手之前先把这几个问题想明白1.1 自动化测试到底解决的是什么问题很多人一说自动化测试脑子里立刻浮现出“造个大框架每天自动跑一堆用例连测试报告都能自动发出来”的宏大画面。但真正常见的痛点反而不是框架而是重复劳动你负责的模块每次发版都要把登录、搜索、购物车流程手工点一遍回归测试清单上有 20 条用例手动执行要半小时点得手指发酸线上出了个 bug你要在本地把某个页面的流程一遍一遍跑纯手工根本没法复现。这些事情本质上都是“人在重复点鼠标”而 Selenium 干的就是把鼠标操作变成代码让浏览器自己跑。这个定位非常重要它决定了你学习 Selenium 时要把精力放在哪里。它不是测试平台不是测试报告系统也不是接口测试工具它就是那个能替你操作浏览器的执行引擎。很多新手学 Selenium 学得痛苦就是因为把它当成万能药什么都想往里塞最后发现定位元素、处理等待、环境配置全是坑。1.2 为什么首选 Python 搭配 Selenium市面上能操作浏览器的自动化工具不少Playwright 和 Cypress 都是后起之秀功能上甚至有超越 Selenium 的趋势。但我带新人入门的时候仍然优先推荐 Python Selenium 组合原因很实在资料密度高。你遇到一个问题不管是元素定位失败还是浏览器驱动版本不匹配几乎都能在几秒钟内搜到现成答案这对初学者太重要了。Python 本身简单。一个用例脚本二十几行读起来像是用自然语言在描述操作步骤不需要绕进复杂的类继承和类型体系。生态成熟。Selenium 支持所有主流浏览器也支持远程分布式启动真到了要扩展的环节你不需要推翻重来。团队协作成本低。测试岗位的人未必都写过硬核代码Python 的写法对非科班出身的人友好得多。当然如果你要做的项目完全基于新起的现代前端框架而且团队对代码质量有要求Playwright 也值得关注。但从“五分钟轻松掌握”这个目标来看Selenium 的入门门槛和容错度是最合适的。我自己维护了几个长期跑着的回归任务至今主力仍然是 Selenium因为它稳定、可控、资料全出问题的时候能快速定位到底是脚本问题还是环境问题。2. 环境搭建把三样东西的关系搞清楚安装就成功了一半2.1 Python、Selenium、浏览器驱动分别扮演什么角色新手在环境搭建这一步最容易栽跟头因为很多人搞不清楚到底要装几个东西。我用一句话总结你负责写代码Selenium 负责把你的代码翻译成浏览器操作指令浏览器驱动负责把指令递给真实的浏览器去执行。所以你会发现一个最简单的环境至少包含三部分Python写脚本的语言环境。Selenium 库用 pip 安装的 Python 包它提供 webdriver 相关的 API。浏览器驱动比如 ChromeDriver它是连接 Selenium 和 Chrome 的桥梁。2.2 按部就班安装的完整步骤第一步是装 Python。去官网下载对应系统的安装包安装时记得勾选“Add Python to PATH”选项。这一步很关键不勾选的话后面在命令行里敲 python 或 pip 会提示找不到命令。装完之后打开终端输入python --version能看到版本信息就说明成功。第二步是安装 Selenium 库。终端里执行pip install selenium如果你用的是 Python 3.4 以上的版本pip 是自带的。如果公司网络慢可以加上国内镜像源比如pip install selenium -i https://mirrors.aliyun.com/pypi/simple/实测速度会快很多。第三步是处理浏览器驱动。过去这一步非常痛苦你要去官网下载与浏览器版本完全匹配的驱动还要手动配置环境变量。好在 Selenium 4.6 开始内置了 Selenium Manager只要你本机装了 Chrome、Edge 或 Firefox它会自动检测浏览器版本并下载匹配的驱动不需要你再手动管。以 Chrome 为例装好后你可以用下面的代码验证环境是否正常from selenium import webdriver driver webdriver.Chrome() driver.get(https://www.baidu.com) print(driver.title) driver.quit()如果这个脚本能弹出浏览器并打印出“百度一下”说明环境已经齐了。我第一次跑通这个脚本的时候最大的感受就是原来“自动化测试”离我这么近它不是一个躺在文档里的抽象名词真的就是这几行代码的事。2.3 环境配置的常见报错与解决办法实际动手时你大概率会遇到下面这几种报错我把排查思路提前给出来报错信息原因分析解决办法ModuleNotFoundError: No module named seleniumSelenium 库没有正确安装确认是哪个 Python 环境执行 pip 时用的哪个 python可以用python -m pip install selenium强制关联当前解释器WebDriverException: Message: unknown error: cannot find Chrome binary浏览器驱动找不到浏览器主程序确认 Chrome 安装路径或直接指定浏览器路径SessionNotCreatedException: This version of ChromeDriver only supports Chrome version X驱动版本不匹配升级 Selenium 到最新版本让它用 Selenium Manager 自动匹配或者手动下载匹配驱动环境安装这块我多说一句新手如果碰到报错不要一上来就卸载重装先看清楚报错信息里出现在哪一行多半是“版本不匹配”或“路径不对”。这两类问题占了我见过的环境类问题八成以上。3. 第一个自动化脚本打开网页、输入关键词、点击按钮、拿到结果3.1 动手写一个能跑的真实用例环境就绪之后我建议你别急着看理论直接写一个能跑、能看结果的小脚本把“自动化测试”这件事先建立起主观感受。这里我选一个最常见的场景在百度首页搜索“自动化测试”然后把第一页结果的标题打出来。import time from selenium import webdriver from selenium.webdriver.common.by import By # 启动浏览器using Selenium Manager 自动管理 driver driver webdriver.Chrome() try: # 访问目标页面 driver.get(https://www.baidu.com) # 找到搜索输入框并输入关键词 search_input driver.find_element(By.ID, kw) search_input.send_keys(自动化测试) # 找到搜索按钮并点击 search_button driver.find_element(By.ID, su) search_button.click() # 等页面加载简单等待 2 秒 time.sleep(2) # 获取页面源码中的返回结果区域文本 result driver.find_element(By.CSS_SELECTOR, #content_left).text print(result[:500]) finally: driver.quit()这段代码做的事情就四步找输入框、输入文字、找按钮、点击然后抓取搜索结果区域文本打印出来。3.2 逐步拆解每一行代码在干什么webdriver.Chrome()创建浏览器实例所有后续操作都基于这个实例。脚本最后必须调用driver.quit()否则会有多个 chrome.exe 残留进程占内存。driver.get()让浏览器访问一个 URL。它只会等待页面 load 事件完成但许多现代网页是 Ajax 动态渲染的所以后面我们还得处理等待问题。find_element(By.ID, kw)按 ID 查找元素。ID 在页面中一般唯一是最稳定的定位方式。send_keys()向输入框发送键盘输入等价于你在页面上打字。.click()模拟鼠标左键点击。time.sleep(2)强制休眠 2 秒是新手最爱用但也最该少用的等待方式后面专门讲。driver.quit()关闭浏览器并清理进程。注意try/except/finally这个结构非常推荐。因为自动化脚本执行过程中只要某一步崩了浏览器窗口很可能一直开着进程一直占着资源。用finally保证无论如何都会关闭浏览器这个习惯能帮你省掉大量烦心事。3.3 浏览器选项打开隐身模式、无头模式、跳过自动化提示很多场景下我们不想弹出浏览器窗口比如在服务器上跑定时任务或者跑回归测试的时候不想让窗口干扰其他工作。这时候可以用Options来配置启动参数from selenium import webdriver from selenium.webdriver.chrome.options import Options options Options() # 无头模式不显示浏览器界面 options.add_argument(--headlessnew) # 跳过网页检测自动化控制提示 options.add_experimental_option(excludeSwitches, [enable-automation]) # 规避部分页面识别为自动化环境 options.add_argument(--disable-blink-featuresAutomationControlled) driver webdriver.Chrome(optionsoptions)无头模式不是“阉割版”它和正常打开浏览器执行的逻辑完全一样只是没有图形界面执行速度通常还快一些。但调试脚本的时候我强烈建议先把无头模式关掉亲眼看着浏览器一步步操作定位问题要快得多。4. 元素定位自动化测试最容易卡壳的地方一张表讲清楚 8 种方式4.1 8 种定位方式的底层逻辑与选择顺序Selenium 官方提供了 8 种定位方式对应By模块里的 8 个常量。我把它们按照推荐程度列出来定位方式写法示例适用场景稳定性IDfind_element(By.ID, search_input)元素有 id 属性很稳定Namefind_element(By.NAME, username)表单类元素常见一般需确认是否唯一Class Namefind_element(By.CLASS_NAME, btn-primary)样式类选择页面经常改样式慎用Tag Namefind_element(By.TAG_NAME, h1)抓取页面结构场景容易命中多个Link Textfind_element(By.LINK_TEXT, 登录)定位带文字的链接直观但中文文案容易变Partial Link Textfind_element(By.PARTIAL_LINK_TEXT, 登)只记得部分文字时不太推荐易误匹配CSS Selectorfind_element(By.CSS_SELECTOR, #username .submit)无 id 但结构清晰的元素性能好、推荐XPathfind_element(By.XPATH, //button[text()确认])按文本/层级结构定位灵活但写复杂了脆弱我的选择顺序是ID 第一CSS Selector 第二XPath 兜底。ID 唯一且稳定CSS Selector 简洁、不支持文本筛选但结构足够XPath 功能最强能按文本位置、父子关系、属性条件各种花式定位但性能稍差写复杂了可读性也差。4.2 XPath 和 CSS 选择器的高频用法先看几个实际开发中会高频用到的 XPath 写法# 根据属性精确定位 driver.find_element(By.XPATH, //input[placeholder请输入用户名]) # 根据文本内容定位按钮 driver.find_element(By.XPATH, //button[contains(text(), 提交)]) # 定位同级元素之后的某个元素 driver.find_element(By.XPATH, //div[classitem]/following-sibling::div[1]) # 按层级定位 driver.find_element(By.XPATH, //div[idapp]//div[classcontent]/span)CSS 选择器常用的几个写法# ID 选择器 driver.find_element(By.CSS_SELECTOR, #app) # 类选择器 driver.find_element(By.CSS_SELECTOR, .submit-btn) # 属性选择器 driver.find_element(By.CSS_SELECTOR, input[placeholder请输入用户名]) # 后代选择器 driver.find_element(By.CSS_SELECTOR, div#app div.content span)一个我自己踩出来的经验是定位表达式能不写死就别写死尤其是 XPath 里的[1]这种位置索引。前端只要是发生小改动优先级最高的计划就是永远不要依赖页面上某个元素是“第几个”。你可以先借助浏览器开发者工具的检查面板去 copy selector但最好还是根据元素本身的稳定属性来写。4.3 元素找不到、定位到多个元素时的应对思路遇到NoSuchElementException绝大多数时候不是 Selenium 的问题而是下面这四种原因元素不在当前页面 DOM 中。页面还没加载完或者元素在 iframe 里。iframe 的情况后面单独讲。定位表达式写错。比如把 class 值写成了标签或者 CSS 选择器的空格用错。元素在隐藏状态。按钮虽然在 DOM 里但可能因为弹窗遮罩、样式隐藏点击时提示拦截。页面发生了跳转你的 driver 还停留在旧页面。遇到定位到多个元素的情况时find_element会返回第一个如果你想要批量操作就得用find_elements它返回一个列表。我用得比较多的地方是抓取列表数据items driver.find_elements(By.CSS_SELECTOR, .result-item) for item in items: title item.find_element(By.CSS_SELECTOR, .title).text link item.find_element(By.CSS_SELECTOR, a).get_attribute(href) print(title, link)4.4 用浏览器的开发者工具辅助定位效率翻倍千万不要靠肉眼看 HTML 源码找元素正确做法是 F12 打开开发者工具的检查器在元素上右键选中“检查”就能直接看到这个元素对应的 HTML 节点。在 Elements 面板里你还能直接右键复制 selector 和 xpath作为起点再手工精简。另一个实用技巧是在 Console 里用 JS 快速验证定位表达式是否正确// 在 Console 里验证 CSS 选择器 document.querySelectorAll(#kw).length // 在 Console 里验证 XPath $x(//input[placeholder请输入用户名])这样能在写 Python 代码之前就确认表达式能不能匹配到能省掉很多在脚本和页面之间来回切的功夫。5. 动态页面与等待机制为什么元素总是时有时无以及怎么彻底解决5.1 三种等待方式的原理对比写自动化脚本时最让人摸不着头脑的问题就是“刚才还跑得好好的换个环境就报找不到元素”。绝大多数情况是页面上的元素是异步加载出来的。这就要讨论等待机制。Selenium 里的等待方式有三种它们的原理完全不同等待方式写法原理坑点强制等待time.sleep(3)进程无条件等待多少秒时间短了不够时间长了拖慢执行网络波动时极其脆弱隐式等待driver.implicitly_wait(10)每次查找元素时如果元素没出现则主动等待直到超过设定时间只能解决“元素出现”的问题不能解决元素可点击、可见、可操作的问题显式等待WebDriverWaitExpectedConditions针对某个条件循环等待直到条件满足或超时需要为关键步骤单独写条件代码多一点但最可靠5.2 显式等待的正确打开方式显式等待是判断元素是否出现、可点击、包含指定文本等最可靠的手段。它的标准写法是这样的from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 10) # 等待输入框可见并可操作 search_input wait.until(EC.visibility_of_element_located((By.ID, kw))) # 等待按钮可点击 wait.until(EC.element_to_be_clickable((By.ID, su))).click() # 等待页面标题变化 wait.until(EC.title_contains(搜索结果))注意显式等待的入参是一个“定位条件元组”也就是(By.ID, kw)不是元素对象。新手常常在这里写错传了元素对象进去就会报TypeError。我习惯在每个关键操作前都用显式等待兜底比如“输入框出现再输入”“按钮可点击再点击”“文本出现再断言”。这个习惯让脚本的抗波动能力提升非常明显。特别是对接第三方系统、网络情况不稳定的环境显式等待几乎是标配。5.3 组合策略最佳实践不是只用某一种实际写脚本时我不太推荐只用某一种等待。一个比较实用的组合是全局设置一个短一点的隐式等待比如 5 秒作为兜底。对关键流程用显式等待明确等待某个条件。彻底别用time.sleep来“补时间”除非你是临时调试。from selenium import webdriver driver webdriver.Chrome() # 全局兜底避免每次 find_element 都因偶发网络闪断而失败 driver.implicitly_wait(5)为什么强调不要用time.sleep因为它本质上是在赌“这个网络下 2 秒一定够”但你没法保证每次执行的环境都跟本地一样。跑测试跑得多的人都有过这种体验本地一把过一到 CI 环境就报错。原因就是 CI 资源紧张加载速度慢固定等待时间不够。换成显式等待直到条件满足才继续这个问题就不存在了。6. 弹窗、iframe、下拉框三个高频“坑”的完整解决方案6.1 非预期弹窗导致失败的根因和处理思路你在搜索一些项目经验或者看社区方案时会频繁看到一个词叫“非预期弹窗导致失败”。这个问题的典型场景是脚本跑得好好的突然某一天开始执行到一半就挂掉报错提示元素不可点击或被拦截。打开浏览器一看屏幕上多了一个活动弹窗、广告浮层、登录引导框或者 cookie 授权提示。这类非预期弹窗的难点在于你无法预知它什么时候出现弹窗也没有稳定的 id。针对弹窗我一般分两层处理第一层是“预防”。给浏览器启动参数加上禁止弹窗的配置尽量减少干扰源。比如 Chrome Options 可以添加options.add_argument(--disable-popup-blocking) options.add_argument(--disable-notifications)第二层是“处理”。如果弹窗还是出现了无论是 alert 原生弹窗还是页面内的 HTML 弹窗模态框都需要代码去识别并关掉。下面是两种最常见的情况。6.2 原生 alert 弹窗的处理原生弹窗包括 alert、confirm、prompt它们在浏览器上是一个独立系统对话框无法通过 find_element 定位。需要用switch_to.alert去接管# 切换并获取 alert 对象 alert driver.switch_to.alert # 读取弹窗文本 print(alert.text) # 点击确认 alert.accept() # 如果是要取消 # alert.dismiss() # 如果是 prompt 弹窗需要先输入文字 # alert.send_keys(输入内容)有一种更稳妥的处理姿势是分段包装一个函数把非预期的 alert 弹窗吞掉def safe_accept_alert(driver, timeout3): try: alert WebDriverWait(driver, timeout).until(EC.alert_is_present()) alert.accept() return True except Exception: return False这样即使某个页面没有弹窗也不会执行到一半因为找不到 alert 而报错。6.3 页面内嵌弹窗HTML 模态框的处理与原生 alert 不同HTML 模态框本身就是页面 DOM 的一部分只是通过遮罩层实现所以仍然可以定位和点击。难点常常是它有动画刚出现时按钮还没稳定到位。处理方式依然是显式等待元素可点击close_btn WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, .modal .close-btn)) ) close_btn.click()如果弹窗的关闭按钮每次都没固定 id可以尝试用相对定位或者用 XPath 按层级查找。如果弹窗里没有明显的关闭按钮可以模拟按 Esc 键from selenium.webdriver.common.keys import Keys driver.find_element(By.TAG_NAME, body).send_keys(Keys.ESCAPE)6.4 iframe 内嵌页面定位不到元素的隐形原因iframe 很坑人的一点是你在主页面 DOM 里根本找不到 iframe 内部的元素哪怕你用 XPath 硬写也写不中因为整个 iframe 是一个独立的文档上下文。处理 iframe 的标准流程就三步切进去、操作、切回来。# 切换到 iframe可以传入 id、name、索引或 WebElement driver.switch_to.frame(iframe_id) # 现在可以定位 iframe 内部的元素了比如按钮 driver.find_element(By.ID, inside_btn).click() # 操作完必须切回主文档否则后续操作全部失效 driver.switch_to.default_content()还有一种嵌套 iframe 的情况需要逐层切driver.switch_to.frame(outer_frame) driver.switch_to.frame(inner_frame)切回来的时候如果只需要回退到上一层可以driver.switch_to.parent_frame()如果要直接回到主页面用default_content()。判断是否在 iframe 里最直接的办法就是用 F12 开发者工具看元素所在的节点有没有被包在iframe标签里。这个排查思路我用过无数次了。6.5 select 下拉框别用 Click 去硬点后面前端项目里用原生的select下拉框的地方越来越少但老系统里很常见。如果它是一个标准 select直接用 Click 去点很容易踩坑因为下拉选项渲染成浮动层时机不好控制。正确做法是引入Select类from selenium.webdriver.support.ui import Select select_element Select(driver.find_element(By.NAME, city)) # 按索引选择0 代表第一个 select_element.select_by_index(1) # 按 value 属性选择 select_element.select_by_value(beijing) # 按可见文本选择 select_element.select_by_visible_text(北京)7. 从跑通一个脚本到能长期维护把自动化测试当成工程来做7.1 用 unittest 或 pytest 组织用例脚本能跑了只是自动化测试的起点。如果只有几个用例直接线性写没问题。但一旦用例超过十个你就会发现维护成本急剧上升改一个元素定位要全局搜索替换执行顺序没法控制失败了一个用例后面全部中断。这时候你应该把脚本组织成测试用例的形式。简单方案是直接用 Python 自带的 unittest不用额外装包import unittest from selenium import webdriver class BaiduSearchTest(unittest.TestCase): def setUp(self): self.driver webdriver.Chrome() def tearDown(self): self.driver.quit() def test_search(self): self.driver.get(https://www.baidu.com) self.assertIn(百度, self.driver.title) if __name__ __main__: unittest.main()如果你更倾向 pytest它的断言写法更简洁fixture 也更灵活社区里用得很广。我的建议是作为个人学习用 unittest 就够了如果要进项目组看团队已有习惯如果没有历史包袱pytest 是更舒服的选择。7.2 元素的复用把定位集中管理我见过太多脚本把同样的定位表达式散落在十几个用例里前端一调整改到想哭。一个比较省心的做法是把关键元素和操作封装成一个页面对象类。不一定要上很重的 Page Object 模式但至少做到“定位字符串集中在一个文件里”。class SearchPage: def __init__(self, driver): self.driver driver property def search_input(self): return self.driver.find_element(By.ID, kw) def search(self, keyword): self.search_input.send_keys(keyword) self.search_button.click()这样做的好处是后续页面改动只需要改这一个类所有依赖它的用例不用动。7.3 断言是自动化测试的灵魂自动化测试和爬虫脚本最大的区别就是测试必须有断言。没有断言的自动化脚本只能叫“自动执行操作”它不能证明结果对不对。在脚本里别只打印输出要看断言是否通过。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待某个关键结果出现出现则说明搜索成功 WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, #content_left)) ) assert 自动化测试 in driver.page_source7.4 测试失败时怎么快速定位问题自动化测试跑久了你会发现“报错不可怕可怕的是报错后不知道发生了什么”。我自己的经验是在任何用例的except分支里把当时页面的截图保存下来同时把页面源代码也保留一份。Selenium 提供了非常方便的办法from selenium import webdriver def take_failure_screenshot(driver, name): driver.save_screenshot(f{name}.png)截图配合报错堆栈基本能还原 80% 的问题现场。如果还看不出原因就把当时页面的 HTML 也导出用 diff 工具和预期模板对比很多时候能直接发现是前端改了文字还是改了结构。7.5 把脚本放到定时任务或 CI 里跑脚本稳定后可以把它交给调度工具。Windows 上的任务计划程序、Linux 上的 crontab或者 GitLab CI、Jenkins 都可以。跑之前注意把无头模式打开这样不会在服务器上弹出一个浏览器窗口。我自己在 GitHub Actions 上都跑过简单的 Selenium 用例配合定时触发器每天自动执行一次然后把结果发到群里。自动化测试的意义恰恰就在这里不是跑一次而是稳定地、自动地、反复地发现问题。7.6 关于“五分钟”的一点实话最后回到这个标题。我必须说实话五分钟能让你把 Selenium 的全貌和第一个脚本跑通但真正能不能用好取决于你接下来有没有耐心去处理定位策略、等待机制、浏览器差异这些细节。我见过很多学了五分钟就觉得自己会了的人结果一到真实项目就被各种奇怪问题打回原形。我自己的体会是自动化测试里七成时间都在处理“为什么找不到元素”和“为什么点击没生效”剩下的三成里一半在写断言一半在看测试报告。把心态摆在这上面你学得反而更快。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进