ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python Selenium Web自动化测试实战:从元素定位到CI集成

Python Selenium Web自动化测试实战:从元素定位到CI集成 前几年我刚转做测试开发的时候第一个正经项目就是给公司的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安装Seleniumpip install selenium如果想用pytest组织和执行用例顺便装这两个库pip install pytest pytest-htmlSelenium 4.x版本对Python 3.7以上的支持都很稳定装起来基本没有什么坑。唯一要注意的是如果你的环境里同时存在Python 2和Python 3pip命令可能指向旧版本最好用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(serviceservice) 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[nameusername])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标签是typefileSelenium直接send_keys文件的本地绝对路径即可触发上传不需要模拟点击文件选择框file_input driver.find_element(By.CSS_SELECTOR, input[typefile]) 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(hookwrapperTrue) 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(), namefailure_screenshot, attachment_typeallure.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 --htmlreports/report.html如果想指定浏览器和并行执行可以用pytest-xdist插件pytest testcases/ -n 3 --distloadscope并行执行时要特别注意浏览器实例之间是隔离的但测试数据可能会互相干扰。比如两个用例同时创建同名的数据就可能导致冲突。所以并行执行前要确保测试数据的独立性或者给每条用例分配唯一前缀。运行结束后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(scopesession) def driver(): driver webdriver.Chrome() yield driver driver.quit() pytest.fixture(scopesession) 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/ --htmlreport.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写错了而是因为你对页面结构的理解还不够。先理解结构再写定位往往一分钟就解了。
返回列表