首页 / 资讯中心 / 文章详情

Selenium与Pytest搭建UI自动化测试框架实战指南

Selenium与Pytest搭建UI自动化测试框架实战指南 ★ FEATURED ARTICLE
做 UI 自动化测试Selenium 和 Pytest 这对组合基本算是最经典的搭配很多团队的第一套 UI 测试框架都是这么起步的。无论你是刚接手一个遗留测试项目还是想从手工点点点里解脱出来这套 Selenium 与 Pytest 结合的框架都能让你用相对少的代码覆盖大量回归场景。我围绕搭建一个可复用的 UI 测试框架把技术选型、目录规划、页面对象封装、Fixture 设计、报告输出和真实项目里踩过的坑全部串起来讲一遍。适合有点 Python 基础、想认真做 UI 自动化的测试工程师也适合在学校或公司里需要快速搭一套可维护框架的学习者参考。先说一个很多人会问的问题网上现成框架那么多比如各种 UI 自动化平台、录制回放工具为什么还要自己搭因为录制回放脚本通常只能做一次性冒烟测试一旦进入真实业务回归元素变化、网络延迟、多账号数据、执行报告这些事没有配套设计根本扛不住。自己搭框架不是追求炫技而是把“能跑的脚本”升级成“能维护的资产”。1. 整体设计与思路拆解1.1 为什么选 Selenium浏览器自动化到底解决什么问题Selenium 的核心能力是用脚本驱动真实浏览器模拟用户在页面上的点击、输入、滚动、切换标签页等操作。它的价值在于验证的是“最终用户看到的东西”而不是接口返回的数据。接口测试可以告诉你后端逻辑对不对但页面布局是否错位、按钮是否被遮挡、异步加载后元素是否出现、登录态是否真正生效这类问题只有落到浏览器层面才能暴露。选 Selenium 而不是其他工具有几个很现实的原因。第一社区生态足够大遇到问题基本都能搜到答案。第二支持主流浏览器Chrome、Firefox、Edge 都有对应的 WebDriver多浏览器兼容测试可以做到一套脚本换参数执行。第三和 Pytest、Allure 这些测试框架整合非常顺不用折腾太多胶水代码。当然它也有缺点比如运行速度慢于接口测试对 CI 机器的资源要求更高但对 UI 回归场景来说这个代价是值当的。1.2 为什么选 Pytest测试框架的底气从哪来Pytest 在这个组合里承担的角色是测试组织者和执行器。它负责发现用例、管理和执行用例、收集测试结果、生成报告。选它的理由很直接断言语法简单不需要像 unittest 那样到处写断言类fixture 机制灵活可以复用浏览器驱动、登录态、测试数据这些前置条件插件生态丰富pytest-html、pytest-ordering、pytest-rerunfailures 都能按需接入。举个例子你可能有 200 条 UI 用例其中 80 条需要登录态20 条需要不同角色账号。用 unittest 写你得自己设计 setUp 和继承关系页面跳来跳去用 Pytest 写只需要在 conftest.py 里定义一个返回浏览器对象和用户身份的 fixture想要的用例直接声明依赖就行。这就是我在项目里坚持用 Pytest 的原因它把“用例自己想要什么前置条件”变成了一种声明式表达从结构上减少用例之间的隐式依赖。1.3 框架分层POM 不是花架子搭建之前先把分层思路定下来。我见过太多失败项目脚本是纯线性的打开页面、输入账号、输入密码、点击登录、断言跳转。所有代码堆在一起换个页面元素就要全文搜索替换改到怀疑人生。后来引入页面对象模型也就是 POM问题才从根本上解决。POM 的思路类似把页面当作一个对象界面上每个可操作元素是这个对象的属性每个业务动作是这个对象的方法。测试用例只关心“去登录页、用某个账号登录、验证登录成功”至于登录按钮叫什么、输入框用什么定位、密码框怎么清空全部封装到页面类里。这样元素一旦变化只改页面类用例代码不用动。这套分层能显著降低维护成本。真实项目里我统计过同样一批功能没有分层时一个版本迭代可能要花半天修定位符用了 POM 后通常只需要改对应页面类里的几行十几分钟就能搞定。除此之外分层还带来一个额外收益新接手的同事不需要理解全部业务细节看一眼页面类就能很快上手修改。2. 环境准备与浏览器驱动配置2.1 创建虚拟环境与依赖安装不管在个人电脑还是 CI 机器上第一步永远是建虚拟环境。Python 的包版本冲突是经典问题你装了一个 Selenium 4 的项目又装了一个基于 Selenium 3 的旧项目不隔离环境必然互相踩包。我自己习惯用 Python 自带的 venv不需要额外安装 virtualenv 工具。python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate激活虚拟环境后安装核心依赖pip install selenium pytest pytest-html安装完可以验证一下版本Selenium 4.x 和 3.x 在部分 API 上有差异。比如 Selenium 4 开始推荐用webdriver.Chrome(serviceService(...))的方式启动浏览器而不是老的executable_path参数。写在项目里的requirements.txt最好锁定大版本范围避免某个小版本更新导致环境不一致。selenium4.0,5.0 pytest7.0,9.0 pytest-html3.0,5.02.2 浏览器驱动那点事Selenium 本身不直接操作浏览器中间还有一层 WebDriver。Chrome 驱动叫 ChromeDriverFirefox 驱动叫 geckodriver它们相当于脚本和浏览器之间的翻译官。最常见的报错有两种找不到驱动路径、驱动版本和浏览器版本不匹配。驱动版本不匹配的报错一般长这样SessionNotCreatedException: This version of ChromeDriver only supports Chrome version xx。解决办法就是去对应下载地址找一个和当前浏览器主版本一致的驱动。比如浏览器是 Chrome 120就下载 ChromeDriver 120 的版本主版本匹配基本就能用。这个坑在项目刚起步时容易踩因为浏览器会悄悄自动升级而驱动不会跟着变。驱动下载后建议放到一个统一目录比如driver/下用代码显式指定路径。不要依赖系统 PATHCI 环境里 PATH 往往没有预置显式指定路径最稳妥。from selenium import webdriver from selenium.webdriver.chrome.service import Service service Service(driver/chromedriver) driver webdriver.Chrome(serviceservice)如果在服务器上跑可以加上无头模式也就是 headless让浏览器在后台运行不弹界面。真实的 CI 环境通常没有桌面headless 是必须的但调试用例时建议关掉它能看到完整界面更容易定位问题。3. 核心封装页面对象模型与 Fixture 设计3.1 BasePage 基类把重复动作收敛到一处框架里最该先写的是 BasePage就是把所有页面都会用到的通用动作抽成基类。等待元素可见、点击、输入、获取文本、截图、滚动到指定位置这些都是高频动作。如果不做基类每个页面类里都写一遍 WebDriverWait代码冗余会非常严重。等待是 UI 测试里最容易出错的地方。很多新手写time.sleep(2)这种固定等待非常不稳定网络慢一秒脚本就挂网络快又白等两秒。正确做法是显式等待比如from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: def __init__(self, driver): self.driver driver self.timeout 10 def find_element(self, by, locator): return WebDriverWait(self.driver, self.timeout).until( EC.visibility_of_element_located((by, locator)) ) def click(self, by, locator): self.find_element(by, locator).click() def input_text(self, by, locator, text): el self.find_element(by, locator) el.clear() el.send_keys(text)实际使用中find_element是核心方法几乎每个页面动作都依赖它。用显式等待的好处是脚本会一直轮询元素状态直到元素出现或超时而不是盲目等固定时间。页面响应快脚本就快页面响应慢脚本最多多等几秒不会一上来就失败。3.2 页面类怎么写元素定位集中管理有了 BasePage写具体页面类就顺理成章了。拿一个最典型的登录页面举例。首先把页面上涉及的元素全部集中定义用元组保存定位方式和定位表达式然后提供业务方法。from selenium.webdriver.common.by import By from base_page import BasePage class LoginPage(BasePage): username_input (By.ID, username) password_input (By.ID, password) login_button (By.ID, login-btn) error_tip (By.CSS_SELECTOR, .error-message) def goto_login_page(self, url): self.driver.get(url) def input_username(self, username): self.input_text(*self.username_input, username) def input_password(self, password): self.input_text(*self.password_input, password) def click_login(self): self.click(*self.login_button) def get_error_tip(self): return self.find_element(*self.error_tip).text注意*self.username_input这种写法就是 Python 的元组解包等价于把 (By.ID, username) 拆成两个位置参数传给input_text。页面类里只关心业务动作不暴露具体定位符这是 POM 的核心。定位方式的选择也有优先级。我个人的优先顺序是ID 优先然后是 name、data-testid 这类专门给测试用的属性再是 CSS 选择器最后才考虑 XPath。绝对路径 XPath 很容易因为页面结构微调而失效能不用就不用。如果项目里前端团队配合最好约定给关键元素加上稳定的>import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service pytest.fixture(scopefunction) def driver(): service Service(driver/chromedriver) _driver webdriver.Chrome(serviceservice) _driver.implicitly_wait(5) yield _driver _driver.quit()scopefunction意味着每个测试用例都拿一个新的浏览器实例用完后自动关闭。这样用例之间完全隔离不会因为上一条用例没有退出登录而影响下一条用例。登录态也不需要保存在全局变量里每条用例需要什么账号就在用例内部完成登录流程代价是多跑几次登录动作但换来了稳定性和可排查性很划算。隐式等待和显式等待可以同时设置但不要把time.sleep混进来。隐式等待设置一次对所有元素生效它表示轮询元素时最长等多久显式等待是针对单个元素做更精确的条件判断。两者搭配使用时注意不要设置过长否则失败用例的排查时间会变得很难受。4. 实操过程从空目录到跑通第一个用例4.1 目录结构从一开始就为维护考虑目录规划直接决定项目后期好不好维护。我推荐下面这种结构简单但够用auto_test/ ├── conftest.py ├── pytest.ini ├── requirements.txt ├── driver/ │ └── chromedriver ├── pages/ │ ├── __init__.py │ ├── base_page.py │ └── login_page.py ├── testcases/ │ ├── __init__.py │ └── test_login.py ├── data/ │ └── login_data.json └── reports/pages 目录放页面类一个页面一个文件。testcases 目录放测试用例通常按模块或页面拆分。data 目录放测试数据比如账号、URL、预期结果。reports 目录放报告和失败截图方便持续集成时上传。pytest.ini 是 pytest 的配置文件写好之后运行命令会简洁很多[pytest] testpaths testcases addopts -vs --htmlreports/report.html --self-contained-html--self-contained-html是 pytest-html 的一个参数生成一个包含全部 CSS 和 JS 的单文件报告便于直接分发或上传到内部平台。4.2 写一个登录用例并跑起来页面类和基类都有了现在写真正能跑通的用例。假设被测系统是一个部署在本地的练习项目地址比如http://demo.test.local/login实际项目里换成自己的环境地址。import pytest from pages.login_page import LoginPage class TestLogin: def test_login_success(self, driver): page LoginPage(driver) page.goto_login_page(http://demo.test.local/login) page.input_username(test_user) page.input_password(test_pass) page.click_login() assert dashboard in driver.current_url def test_login_wrong_password(self, driver): page LoginPage(driver) page.goto_login_page(http://demo.test.local/login) page.input_username(test_user) page.input_password(wrong_pass) page.click_login() assert page.get_error_tip() 用户名或密码错误用例的逻辑非常干净就是按照业务步骤描述的方式去操作页面。运行方式也直接pytest testcases/test_login.py -v看到2 passed说明用例跑通了。如果环境不稳定可以用 pytest-rerunfailures 插件控制失败重跑但别把重跑次数设太高它只是缓解网络抖动和偶发元素加载慢的问题真实失败还是要靠报告分析。4.3 用参数化把用例变成数据驱动场景实际项目里登录用例不可能只测一个账号。用户名的边界值、密码为空、锁定账号、多角色权限这些场景的步骤完全一样只有数据不同。用参数化可以避免复制粘贴用例import pytest pytest.mark.parametrize(username,password,expected_url_no, [ (admin, admin123, admin), (guest, guest123, guest), ]) def test_login_with_role(self, driver, username, password, expected_url_no): page LoginPage(driver) page.goto_login_page(http://demo.test.local/login) page.input_username(username) page.input_password(password) page.click_login() assert expected_url_no in driver.current_url参数化的意义不只是减少代码量更重要的是让测试数据和测试逻辑分离。后续如果需要新增一批测试数据只需要改测试装饰器里的参数列表不需要动用例逻辑。数据量大的时候还可以把数据放到 JSON 文件里用 pytest 的 fixture 读取让用例和数据的耦合进一步降低。5. 常见问题与排查技巧实录5.1 高频报错速查表UI 自动化最耗时间的阶段不是写脚本而是排查失败。下面这些报错几乎每个 Selenium 项目都会遇到列成表格方便对照定位报错信息常见原因处理办法NoSuchElementException元素定位表达式错误、元素未加载完成、元素在 iframe 中先加显式等待再核对定位表达式最后检查是否在 iframeTimeoutException显式等待超时元素没有在预期时间内出现打开页面看真实的加载流程确认是否有多步交互触发元素出现StaleElementReferenceException页面刷新或 DOM 更新后之前拿到的元素已经失效重新定位元素不要缓存 WebElement 对象跨步骤使用SessionNotCreatedException浏览器和 WebDriver 版本不匹配下载和当前浏览器主版本一致的驱动ElementClickInterceptedException点击被其他元素遮挡常见于 loading 遮罩、弹窗、广告等待遮罩层消失或改用 JS 点击WebDriverException驱动路径错误、驱动无法启动检查 Service 路径是否正确浏览器是否安装在默认位置排查顺序也有讲究。先看是元素没出现还是定位不对把超时时间临时调大跑一次如果调大后能过说明是等待策略不够完善如果时间很长还是不过大概率是定位表达式写错了用浏览器开发者工具再核对一遍。5.2 稳定性优化的几个实操习惯真实项目里UI 用例容易偶发失败。你跑一次全绿再跑一次红了一个用例重跑又绿了。这种情况非常磨心态。经过长期实践我总结出几个能显著提升稳定性的习惯。第一不要用固定睡觉时间来等待所有等待都换成显式等待。固定等待的脚本在低负载环境一切正常到了高负载环境就批量超时而且报错时间都集中在 sleep 结束时极具误导性。第二优先使用稳定的定位表达式。我在某公司做内部系统测试时有一个按钮的 XPath 是绝对的html/body/div[1]/div/div[2]/form/...页面一改版就挂。后来统一改用>pytest.hookimpl(tryfirstTrue, 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: driver.save_screenshot(reports/failure_{}.png.format(item.name))代码逻辑不复杂pytest 每跑完一个测试步骤都会调用这个 hook我们判断是否是最终失败阶段是的话就从 fixture 参数里取出 driver把画面存下来。配合 pytest-html 报告效果很好。5.3 一个真实踩坑案例点击被 loading 遮罩拦截有一次测试某个数据量较大的列表页点新增按钮后页面会弹出一个全屏 loading 遮罩数据加载完成遮罩才消失。脚本逻辑很直接点击按钮、等待表单出现、输入数据。但执行到表单输入时偶尔报错说找不到输入框。排查了很久发现并不是表单加载慢而是 loading 遮罩还盖在页面上。Selenium 的visibility_of_element_located对这种透明遮挡层是无效的因为输入框本身已经可见但点击被遮挡层拦截导致事件没有真正落到输入框上。后来改成等待遮罩层消失再继续操作问题就消失了。这个案例说明一个道理页面元素可见不代表可以直接操作。现代前端框架的异步渲染带来很多类似问题书写等待条件时不仅要关注目标元素还要关注影响操作的遮挡元素。在复杂页面上多花时间梳理真实交互流程远比多写几行代码更有价值。6. 报告输出与日常维护建议6.1 pytest-html 与失败截图联动pytest-html 生成的报告是 HTML 页面包含每个用例的执行结果、耗时、失败信息。配合上一节截图失败的 hook报告里每一项失败用例都能看到对应的错误堆栈和现场截图。执行完一轮回归我一般直接打开报告定位红项点进去看崩溃瞬间的页面状态很快能判断是环境问题还是真实缺陷。如果项目对报告美观度有更高要求可以换成 Allure它是一个更专业的测试报告框架支持历史趋势、用例分级、标签筛选。但 Allure 需要额外安装工具和命令行包对于十几条用例的小项目性价比不高。pytest-html 足够覆盖大部分场景。6.2 一份给新手的日常维护清单最后分享一点维护心得。UI 自动化框架搭建完成只是开始后续维护才是真正的重头戏。我给自己定了几条规矩每次修改产品页面元素后至少每周跑一次全量回归及时暴露因页面改版导致的用例失效每次跑完失败用例一定要分析根因而不是直接重跑碰运气新增页面时先把页面类写好再写用例不要先写用例再补封装。还有一条非常重要保持自动化用例和手工测试的互相印证。UI 自动化容易陷入只测主流程的误区渐渐偏离真正的高风险场景。定期把手工测试发现的问题与自动化用例覆盖范围做对比发现盲区就补用例发现失败多的模块就重点优化这套框架才会越用越顺手。对我个人来说这套 Selenium 加 Pytest 的组合已经陪伴我扛过好几个项目的回归压力希望你搭出自己的框架后也能体验到那种“跑完一轮全绿”的踏实感。
阅读完成 · 觉得有帮助?
咨询建站