Python Selenium Web自动化测试实战:从元素定位到CI集成
2026/9/9 10:29:40 网站建设 项目流程

前几年我刚转做测试开发的时候,第一个正经项目就是给公司的Web管理后台做自动化测试,技术栈定的就是Python加Selenium。说实话,那时候我对这个组合的理解还停留在“能打开浏览器、能点两下按钮”的层面,等真正把一套用例跑起来、融入CI流程之后才发现,Selenium能做的事情远比想象中多,但坑也远比教程里写的多。这篇文章我就把整个项目的设计思路、核心代码、踩坑记录完整梳理一遍,如果你正准备上手Web自动化测试,或者已经被Selenium的稳定性问题折磨过一段时间,应该能从中找到一些能直接抄作业的东西。

先说清楚这个项目解决的是什么问题。我负责的那个后台系统,每次发版前需要人工回归一遍核心流程,包括登录、权限校验、列表查询、数据导入导出、配置保存等等,一个人全手工跑一遍大概要两到三个小时,而且重复劳动特别容易漏点。用Selenium把这套流程脚本化之后,单次回归压缩到十几分钟,还解放了人力去做探索性测试。对于测试团队已经积累了大量手工用例、希望快速建立自动化回归能力的场景,Python加Selenium是性价比非常高的入门方案,因为它上手门槛低、生态成熟、社区资料多,团队里随便一个会写脚本的人都能快速上手。

至于这套方案适合谁来学,我建议分三类:一类是刚入门测试开发、想搞懂浏览器自动化原理的新人;一类是手工测试做到一定瓶颈、想用工具提升效率的测试工程师;还有一类是开发团队想自建轻量级UI验证体系但没有太多人力投入的情况。不管哪类读者,只要跟着把环境配好、把元素定位和等待逻辑吃透、再跑通一个完整的用例,就已经算入了Web自动化的门。

1. 项目整体设计与方案选型

1.1 为什么选择Selenium而不是其它自动化框架

有段时间 Playwright 和 Cypress 的风头很盛,尤其是 Playwright,自带自动等待、多浏览器支持、拦截网络请求这些特性,确实比 Selenium 原生API好用不少。那为什么我在这个项目里还是选了 Selenium?核心原因是项目的存量资产和团队技能栈。

当时团队里已经有几十条用Selenium写的历史脚本,虽然维护得不算好,但至少能跑。如果推到重来换Playwright,意味着这些用例全部要重写,时间成本直接翻倍。另外Selenium支持的语言非常全,Java、Python、C#、JavaScript都有官方绑定,团队里不管谁想参与都能快速上手。如果是一个从零开始的新项目、且团队愿意接受新工具,Playwright确实值得考虑,但如果是已有资产复用、团队技能参差不齐的场景,Selenium依然是最稳妥的选择。

还有一点很实在:Selenium的社区问答积累非常厚。我在写脚本过程中遇到的各种元素定位失败、驱动版本不匹配、iframe切换异常问题,几乎都能在社区里找到现成答案。小众框架遇到诡异问题可能连提问都描述不清楚,这在企业项目里是很要命的。

1.2 自动化测试金字塔与项目诉求的匹配

做Web自动化最忌讳一件事,就是想把所有手工用例全部自动化。我在立项初期就明确了一条边界:Selenium只覆盖核心业务链路的回归验证,不追求UI层面的全覆盖。

经典的自动化测试金字塔模型建议的是“底层单元测试最多、接口测试其次、UI自动化最少”,因为UI自动化最脆弱、执行最慢、维护成本最高。放到我那个后台项目里,具体落地策略就是:登录、权限、列表筛选、数据导出这种高频且流程明确的场景,纳入Selenium的回归范围;而像表单校验、复杂交互逻辑这种细枝末节,优先交给接口测试去覆盖。这样设计既能保证核心链路不出大问题,又不会让自动化脚本的维护成本拖垮迭代节奏。

1.3 技术栈与工具链全景

这个项目最终的技术栈组成大概是下面这个清单,不算复杂,但每层都有自己的职责。

  • Python 3.9及以上:脚本语言,开发效率高,生态丰富
  • Selenium 4.x:浏览器自动化核心库
  • pytest:测试框架,负责用例组织、断言、fixture管理
  • WebDriver Manager:自动管理浏览器驱动版本,省去手动下载的麻烦
  • Faker:生成测试数据,避免硬编码
  • Jenkins:集成CI,实现定时触发和构建后自动执行
  • Allure:测试报告展示,直观看到每一步的执行结果

这套组合的好处是每一层都有成熟的替代方案,哪怕你所在的团队不用Jenkins,也可以换成其它CI工具,甚至本地定时任务直接跑,核心的Selenium部分完全不受影响。

2. 环境准备与浏览器驱动配置

2.1 Python与Selenium库的安装

环境准备是整个项目里最容易出问题、但又最容易被忽略的一步。我见过不少新手卡在浏览器驱动上,半天打不开浏览器,最后发现是驱动版本和浏览器对不上。所以这里我把安装过程拆开,一步一步说清楚。

首先是Python环境。如果你用的是Windows,推荐直接去官网下载安装包,安装时勾选“Add Python to PATH”;如果你用的是Mac或Linux,系统自带Python通常版本较老,建议用包管理器安装新版本Python 3.10以上。装好之后在终端验证一下:

python --version

确认版本没问题后,用pip安装Selenium:

pip install selenium

如果想用pytest组织和执行用例,顺便装这两个库:

pip install pytest pytest-html

Selenium 4.x版本对Python 3.7以上的支持都很稳定,装起来基本没有什么坑。唯一要注意的是,如果你的环境里同时存在Python 2和Python 3,pip命令可能指向旧版本,最好用python3 -m pip install这种方式显式指定。

2.2 浏览器驱动:新手最容易翻车的环节

Selenium本身不会操作浏览器,它需要一个“中介”去驱动浏览器,这就是WebDriver。Chrome对应chromedriver、Firefox对应geckodriver、Edge对应msedgedriver。

早年最麻烦的是需要手动去下载驱动文件,然后放到系统PATH里,或者启动时指定路径。这里有两个高频坑:一是下载的驱动版本和浏览器版本不匹配,直接报SessionNotCreatedException;二是驱动文件放的位置不对,启动时报WebDriverException找不到驱动。

我在项目里做了一件一劳永逸的事:用WebDriver Manager自动处理驱动。它会在运行时检测你本机浏览器的版本,自动去下载匹配的驱动,不需要你手动维护任何文件。解决这个问题,直接用下面的代码:

from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) driver.get("https://example.com")

首次运行会自动下载驱动,耗时稍长,但之后就都是秒开了。配上这个方案,整个团队的开发机环境可以做到几乎零配置,新同学拉下代码就能跑。

2.3 Selenium 4中的Selenium Manager

如果你用的是Selenium 4.6及以上版本,官方已经内置了Selenium Manager,它会在驱动缺失时自动下载和管理驱动。也就是说,上面那套WebDriver Manager的代码其实还可以简化,直接这样写就能跑:

from selenium import webdriver driver = webdriver.Chrome() driver.get("https://example.com")

不需要指定Service、不需要手动下载驱动,Selenium Manager会自动匹配本机Chrome版本并下载对应的chromedriver。这个特性极大降低了入门门槛,我在这个项目的后半程已经全面切到这种方式。

不过有一点要提醒:Selenium Manager依赖网络下载驱动,如果你的公司网络有严格限制,可能还是需要手动放驱动文件,或者配置内部的镜像源。这种偏门情况虽然少见,但真遇到的时候也够折腾一阵的。

3. 核心功能解析与实操要点

3.1 元素定位:能不能跑稳全靠这一环

元素定位是整个Selenium脚本的核心,定位不到元素就什么都做不了。Selenium提供八种定位方式,实际项目里核心就那几种。

最推荐的是ID定位,稳定且速度快。比如登录页的输入框如果有一个唯一的id,直接用:

driver.find_element(By.ID, "username")

没有ID的情况下,优先考虑CSS Selector和XPath。CSS Selector语法简洁,性能一般也优于XPath,适合按class、属性、层级关系定位:

driver.find_element(By.CSS_SELECTOR, ".login-form input[name='username']")

XPath最灵活,可以基于文本内容定位,比如一个按钮的文案是“提交”,就可以用:

driver.find_element(By.XPATH, "//button[contains(text(), '提交')]")

这里我想强调一个理念:定位方式的选择优先级应该是最小化对页面结构的依赖。所谓最少的依赖,就是优先用ID、name这类语义化属性;其次用CSS;最后才用XPath文本定位,因为现代前端框架会频繁刷新列表项,文本定位在页面更新后非常容易失效。项目里早期的脚本大量使用XPath绝对路径,后来页面一改版就全挂,我花了不少时间重构。记住一句话:绝对路径绝对不要用,一个层级变化整条用例就废了。

3.2 等待策略:把强制sleep戒掉

新手写Selenium最容易犯的错就是上来一个time.sleep(2),页面加载快慢不定,等少了报找不到元素,等多了白白拖慢用例。正确做法是掌握三种等待方式,并用好两种“聪明”等待。

显式等待是最推荐的方式,它会在指定时间内轮询元素状态,一旦满足条件立即继续执行:

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) element = wait.until( EC.element_to_be_clickable((By.ID, "submit-btn")) )

上面这段代码的意思是:最多等10秒,但每0.5秒检查一次,按钮一旦变成可点击状态就立刻返回。这样既不会因为等待时间不够而失败,也不会白白多等。

隐式等待是一个全局配置,设置一次对后续所有find_element都生效,但它的粒度和灵活度都不如显式等待:

driver.implicitly_wait(10)

我的建议是:在项目初始化时设置一个隐式等待作为兜底,针对关键步骤和异步加载场景使用显式等待。两者同时使用时,实际等待时间是两者中的较大值,所以数值不要设得过于夸张。

还要提一个特殊等待,就是页面元素已经存在但动画还在进行的情况。比如弹窗渐入,元素已经可以获取但点击会失败,这时可以等一下元素可见且可点击,或者干脆用显式等待去等待一个动态出现的loading标识消失。

3.3 页面交互:点击、输入、下拉选择与文件上传

常见的Web操作在Selenium里都有对应的API,我挑几个高频场景说说。

输入文本用的是send_keys,但要注意两点:一是输入前先clear清空,防止历史文本残留;二是部分自定义组件需要先点击组件内部某个地方再输入,直接send_keys可能无效。清空是很多人容易漏掉的细节,特别是编辑场景,不清空就会得到“历史值+新值”的拼接:

username_input = driver.find_element(By.ID, "username") username_input.clear() username_input.send_keys("test_user")

下拉选择框,原生select可以直接用Select类:

from selenium.webdriver.support.ui import Select select = Select(driver.find_element(By.NAME, "category")) select.select_by_visible_text("技术类")

如果下拉是自定义组件,就需要先点击展开,再点击选项,本质上还是普通的元素操作。

文件上传是一个很容易被问到的点。如果input标签是type=file,Selenium直接send_keys文件的本地绝对路径即可触发上传,不需要模拟点击文件选择框:

file_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']") file_input.send_keys("C:\\data\\test_users.xlsx")

如果上传组件是自定义的,隐藏了input,那通常需要先对隐藏元素执行取消隐藏的操作,或者直接通过JavaScript把文件路径塞进去,这种情况要单独再讨论,但大多数后台管理系统的上传组件都保留了原生input,上面这个方法够用。

3.4 断言与测试报告:不能只跑不判

脚本能跑通只是第一步,跑完之后到底验证了什么,靠的就是断言。pytest框架里最常用的是assert语句。我在项目中通常会做三层断言:

第一层是页面行为断言,比如登录成功后,判断右上角是否出现用户名:

assert "欢迎 admin" in driver.page_source

但page_source获取整个页面源码性能较差,更推荐定位到具体元素再断言文本:

welcome_text = driver.find_element(By.CLASS_NAME, "user-name").text assert welcome_text == "admin"

第二层是数据断言,比如列表里新增了一条记录,检查表格里是否存在对应文本。这种断言需要深入到表格内部定位,比单纯判断页面源码可靠。

第三层是URL断言,比如登录失败后,确认URL还是停留在登录页,或者重定向到了错误提示页:

assert "/login" in driver.current_url

测试报告我推荐Allure,它可以给每一步操作添加说明和截图。关键是加一行监听器,让用例失败时自动截图:

# conftest.py import allure import pytest from selenium import webdriver @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() if report.when == "call" and report.failed: driver = item.funcargs.get("driver") if driver: allure.attach( driver.get_screenshot_as_png(), name="failure_screenshot", attachment_type=allure.attachment_type.PNG )

截图的存在不是给你自己看的,是给开发和其他测试看的,一张失败截图配合堆栈信息,能让问题定位速度快上一大截。

4. 实操过程:用一个完整登录链路示例串联整个流程

4.1 被测场景与测试数据准备

光讲API太散,我拿一个最经典的管理后台登录链路来演示。被测场景是:打开登录页 -> 输入用户名和密码 -> 点击登录 -> 验证跳转到首页 -> 退出登录。这个流程虽然简单,但能覆盖元素定位、等待、断言、清理环境等多个关键环节。

测试数据方面,我准备了两种数据源。一种是固定的测试账号密码,写在配置文件中,方便统一管理;另一种是用Faker生成随机用户名,用来验证注册和重复性校验等场景。测试数据管理的原则是不要在代码里硬编码,至少要做到集中管理,后续切换测试环境时改一处配置即可。

# 项目目录结构 web_auto_test/ ├── conftest.py # pytest fixtures ├── config.yaml # 环境配置 ├── pages/ # Page Object页面对象 │ ├── login_page.py │ └── home_page.py ├── testcases/ # 测试用例 │ ├── test_login.py │ └── test_home.py ├── utils/ # 工具函数 │ ├── driver.py │ └── data_gen.py └── reports/ # 测试报告输出

这种结构不是硬性要求,但如果你想把自动化测试长期维护下去,建议从第一天就按照这个思路来整理。

4.2 Page Object模式:把页面和用例分离

写Selenium用例最怕的就是几十个用例里到处是driver.find_element,页面一改版,几十处全得跟着改。Page Object模式(简称PO模式)就是解决这个问题的,它的核心思想是“每个页面一个类,把该页面的元素定位和操作封装在类里,测试用例只调用这些方法”。

用一个登录页封装示例来说明:

# pages/login_page.py from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class LoginPage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def open(self, url): self.driver.get(url) def input_username(self, username): el = self.wait.until(EC.presence_of_element_located((By.ID, "username"))) el.clear() el.send_keys(username) def input_password(self, password): el = self.wait.until(EC.presence_of_element_located((By.ID, "password"))) el.clear() el.send_keys(password) def click_login(self): el = self.wait.until(EC.element_to_be_clickable((By.ID, "login-btn"))) el.click() def login(self, username, password): self.input_username(username) self.input_password(password) self.click_login()

测试用例就变得非常简单和可读:

# testcases/test_login.py import pytest from pages.login_page import LoginPage def test_admin_login(driver): login_page = LoginPage(driver) login_page.open("https://admin.example.com/login") login_page.login("admin", "123456") assert "首页" in driver.page_source

这个模式最大的好处是:当登录页的某个输入框ID发生变化时,只需要改LoginPage这一个文件,所有用到登录操作的用例都不需要动。我在实际项目中深有体会,没有PO模式之前,一次样式改版可能要花半天去修脚本;有了PO模式之后,基本十几分钟搞定。

4.3 完整脚本运行与结果解读

用pytest执行用例,最简单的命令是这样:

pytest testcases/ -v --html=reports/report.html

如果想指定浏览器和并行执行,可以用pytest-xdist插件:

pytest testcases/ -n 3 --dist=loadscope

并行执行时要特别注意:浏览器实例之间是隔离的,但测试数据可能会互相干扰。比如两个用例同时创建同名的数据,就可能导致冲突。所以并行执行前要确保测试数据的独立性,或者给每条用例分配唯一前缀。

运行结束后,pytest-html的report.html是纯HTML页面,浏览器直接打开就能看到结果;如果用Allure,还可以把结果转成更直观的HTML报告:

allure generate reports/allure-results -o reports/allure-report --clean

报告里可以清楚看到每条用例的通过状态、执行时间、日志和失败截图。我拿到报告后的第一件事永远是看失败截图的分布,如果某个页面在多个用例里同时失败,那大概率不是用例的问题,而是环境或页面本身有Bug,这个时候测试的价值就体现出来了。

5. 常见问题与排查技巧实录

5.1 高频问题速查表

我在维护这个项目半年多的时间里,遇到过很多奇奇怪怪的问题,有些问题反复出现,我整理成了一份速查表,方便你对照排查。

现象可能原因排查方向
启动浏览器时报SessionNotCreatedException浏览器驱动与浏览器版本不匹配用Selenium Manager或WebDriver Manager自动匹配版本
明明页面有元素却报NoSuchElementException页面使用iframe或shadow DOM,元素不在默认文档流里切换到对应iframe后再定位
元素找到了但点击无效页面有遮罩层或元素被遮挡显式等待遮罩消失,或用JavaScript强制点击
脚本执行速度越来越慢每次操作前强制sleep时间过长全面替换成显式等待
偶尔通过偶尔超时依赖真实网络环境或第三方接口返回不稳定增加更宽容的等待条件,或对已知波动接口做重试
浏览器窗口一闪而过就消失启动参数错误,代码异常导致进程退出在代码里不要立刻quit,先定位问题,再加try/finally处理
中文文本显示乱码页面编码问题或读取字符编码不对确保页面charset声明正确,脚本以UTF-8保存

表格里这些现象,80%的自动化脚本问题都能覆盖到,真遇到了可以逐条对照排查。

5.2 iframe与多窗口切换:两个绕不开的坎

现代管理系统里iframe用得不算多,但一旦遇到就会让新手卡很久。如果一个元素明明在页面上,却始终定位不到,很大概率是它被嵌在iframe里。处理方法是先切进iframe,操作完成后再切回来:

driver.switch_to.frame("content_iframe") # 操作iframe里的元素 driver.find_element(By.ID, "edit-name").clear() # 切回主文档 driver.switch_to.default_content()

iframe的切换就像是进入了一个子房间,你在子房间里做完事,必须回到大厅,不然接下来找主文档里的元素就找不到了。

多窗口的情况与之类似。点击一个新链接后,网站可能在新的标签页打开页面,这时需要用窗口句柄切换:

handles = driver.window_handles driver.switch_to.window(handles[-1])

我建议每次切换前先打印当前窗口句柄和所有句柄,确认切换目标,避免在多窗口之间来回跳跃时搞混。

5.3 稳定性提升的三个实战技巧

自动化脚本跑得稳不稳定,很大程度上取决于代码对真实页面的适应能力,这几点是我的深刻经验。

第一,按钮点击前先等待元素可点击,这里用显式等待。但有些前端框架使用了过度动画,即使元素已可点击,点击事件也可能在动画中丢失。针对这种情况,我会在点击前额外加一小段极短的停顿,比如0.3秒,成本极低但能明显降低失败率。

第二,对于动态生成的ID,比如每次刷新页面组件ID都会变化,不要硬靠ID定位,改用class、文本组合或者相对层级定位。这个在表格操作中特别常见,一条记录的“编辑”按钮,其ID往往包含该记录的主键,这种ID用于定位就是失灵的,正确的做法是定位到该行,然后在该行上下文范围内找按钮,用xpath的祖先兄弟关系来定位。

第三,登录态管理。很多用例不需要重复登录,但也不能指望浏览器会话一直有效。我的做法是:在conftest.py里做一个session级别的fixture,登录成功后保存cookie,后续用例复用这个cookie来建立会话,大幅减少登录消耗的时间。

# conftest.py import pickle import pytest from selenium import webdriver @pytest.fixture(scope="session") def driver(): driver = webdriver.Chrome() yield driver driver.quit() @pytest.fixture(scope="session") def login_cookie(driver): driver.get("https://admin.example.com/login") driver.find_element(By.ID, "username").send_keys("admin") driver.find_element(By.ID, "password").send_keys("123456") driver.find_element(By.ID, "login-btn").click() time.sleep(1) cookies = driver.get_cookies() with open("cookies.pkl", "wb") as f: pickle.dump(cookies, f) return cookies

通过cookie复用的方式,原本每分钟跑一次的冒烟测试,整体耗时能压缩一半以上。

6. 与CI的集成:让自动化脚本成为回归检查的守门员

6.1 本地定时执行与Jenkins集成

脚本写完不是终点,真正让它发挥价值的是定时自动执行。我在项目里接入的是Jenkins,流程是:开发提交代码后触发构建,构建完成后自动执行测试脚本,测试结果推送给对应负责人。

Jenkins接入方式不复杂,项目里创建好虚拟环境和依赖清单requirements.txt,然后在Jenkins的构建步骤里执行:

pip install -r requirements.txt pytest testcases/ --html=report.html

构建之后把report.html作为构建产物归档。这样每个版本构建后,测试报告就自动生成,不需要任何人手动干预。

对于没有Jenkins条件的小团队,也可以用Windows任务计划或crontab做定时执行,再配合邮件或企业微信通知。自动化测试的落地不一定要大而全的工具链,能跑起来、能通知到人,就已经体现了很大价值。

6.2 失败重跑与结果治理

UI自动化最烦的是偶发失败,同一个用例同一环境跑十次挂一两次。如果直接把失败用例推给开发,很容易让人对自动化报告失去信任。我在项目里为pytest配置了失败重跑机制:

pytest testcases/ --reruns 2 --reruns-delay 1

这样每个用例最多重试两次,如果重试后通过,说明只是环境抖动;如果重试后仍然失败,大概率是功能层面的Bug。

但也要注意,重跑机制不能滥用,尤其不能在所有用例上都无脑加。那些本身就依赖数据唯一性的用例,重跑可能导致数据重复而二次失败。通常是先在本地跑一遍,确认稳定后再在CI上开启重跑。

6.3 测试数据准备与清理规范

自动化测试要稳定,数据处理必须规范。我在项目里定的规矩是:每条用例执行前通过接口或数据库创建自己的测试数据,执行完毕后清理数据。这样即使并行执行,也不会出现用例之间互相干扰的问题。

如果依赖UI去创建前置数据,那等于把你想要测试的流程又跑了一遍,用例执行时间成倍增加,而且前置数据创建失败会造成假失败。正确思路是:能用接口创建的不要用UI创建,能用SQL清理的不要依赖UI删除。这也是测试金字塔思想的延伸——UI只负责验证UI,数据准备交给更底层的手段去完成。

我以前在一个项目里,为了测一个列表搜索功能,UI里要先创建100条数据,光创建数据就要花五分钟,整个用例跑下来又慢又脆弱,后来全部改成接口造数,测试效率提升了不止一个量级。这是一个很值得记住的经验。

7. 写在最后的个人实践心得

跑了大半年的Selenium自动化之后,我最大的体会就是:自动化测试的核心不在于把每个操作写成代码,而在于取舍和设计。哪些用例值得自动化、哪些场景用接口测试就好、哪些页面改动频率高不适合做UI脚本,这些问题在动手写第一行代码前都应该想清楚。

另外我个人强烈建议,每一条用例都要在真实的Chrome浏览器下跑过至少十次以上,确认稳定了再去考虑并行和CI。Selenium脚本是有概率性问题的,一次通过不代表永远能通过,只有把那些玄学失败压制到最低,自动化测试报告才有说服力。

最后再分享一个小技巧:如果你遇到某个元素定位问题,怎么都解决不了,别一直埋头调xpath。先在浏览器开发者工具里看一下目标元素的完整结构,确认它到底是不是在iframe里、有没有shadow DOM遮挡、是不是动态渲染的。定位不出元素很多时候不是因为xpath写错了,而是因为你对页面结构的理解还不够。先理解结构,再写定位,往往一分钟就解了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询