资讯动态

Python+Selenium+Pytest:Web UI自动化测试框架实战

发布时间:2026/10/18 13:09:16 来源:尧图企业网站定制
想用 Python 做自动化测试Selenium 和 Pytest 基本是绕不开的两个名字。Selenium 管浏览器操作Pytest 管用例组织、断言和报告再加上 Python 的语法红利这三样组合起来几乎成了 Web UI 自动化测试的默认技术栈。哪怕现在各种无人测试平台、AI 测试工具层出不穷这个组合依然是绝大多数团队测试框架的底座也是新人入行最值得先啃下来的一套技能。这篇内容会围绕一个完整的实战案例来展开从环境准备到框架搭建从一条登录用例写到参数化、报告输出再到我在实际项目中踩过的坑。不整虚的直接按你能跑通、能抄走的标准来写。1. 框架设计与技术选型这个组合为什么这么能打先聊点背景。很多人一开始纠结到底用 Java 还是 Python 做 UI 自动化其实没有标准答案但 Python 在国内的测试圈里普及率明显更高。原因很朴素写起来快维护成本低第三方库生态全而且团队里其他同事读代码的门槛也低。Selenium 作为浏览器自动化的老牌方案支持 Chrome、Firefox、Edge 这些主流浏览器API 稳定社区资料多到翻不完。Pytest 则负责把测试代码组织成规则清晰的用例提供断言、固件fixture、参数化这些能力让一套脚本真正变成一套可维护的测试系统。1.1 三个工具的分工逻辑简单说这个三件套各管一段缺一个都不顺畅。Selenium 解决的是怎么操作浏览器的问题。打开页面、点击按钮、输入文字、获取弹窗、切换 iframe、处理多窗口这些动作都属于 Selenium 的职责范围。很多人以为 Selenium 是自动化测试的全部其实它只是最底层的驱动层。它不管你这条用例跑没跑过也不负责生成报告这些判断和管理工作得靠其他工具来完成。Pytest 解决的是测试怎么组织的问题。它负责收集用例、控制执行顺序、管理前后置操作、提供断言方法。我们写一条用例断言页面跳转对不对、提示文案是不是预期的这些能力都由 Pytest 或者 Python 自带的 assert 来提供。Pytest 还支持 fixture能很好地解决用例之间公共前置操作的问题比如启动浏览器、登录拿到 session这些都能封装成 fixture 供多条用例复用。还有不少被忽略的辅助工具但它们是这个组合能真正落地的关键。比如 WebDriver 管理器我用的是 webdriver-manager 这个库比如数据驱动用的 YAML 或 Excel 读取库再比如报告生成插件 pytest-html。后面都会展开讲。1.2 项目结构从第一天就按工程标准来很多新手写自动化测试习惯把脚本全部堆在一个文件里比如 login_test.py 里面写了 200 行。这在 demo 阶段没什么问题但一旦用例量上来维护成本会非常痛苦。我通常建议从搭建框架的第一天起就按照分层结构来组织代码避免后期重构。一个比较成熟的基础结构是auto_test_demo/ ├── pages/ # 页面对象层 │ ├── __init__.py │ ├── login_page.py │ └── home_page.py ├── test_cases/ # 测试用例层 │ ├── __init__.py │ ├── conftest.py │ └── test_login.py ├── data/ # 测试数据或其他配置文件 │ ├── login_data.yaml │ └── config.yaml ├── utils/ # 公共工具类 │ ├── driver_utils.py │ └── yaml_utils.py ├── reports/ # 测试报告输出目录 ├── logs/ # 运行日志目录 └── requirements.txt这里面有两个核心的分层思想值得重点理解。第一是页面对象模式Page Object Model简称 POM就是把一个页面的元素定位和操作动作封装成一个类。比如 LoginPage 负责登录页上所有元素的定位和填写逻辑HomePage 负责登录成功后首页的断言逻辑。好处非常明显如果开发改了页面元素我们只需要改 pages 里对应的那一个类而不需要去翻几十条用例逐个修改。这个模式是 UI 自动化的核心武器越早掌握受益越大。第二是 conftest.py 的全局 fixture 管理。Pytest 会自动加载 conftest.py 里的 fixture不同目录层级可以有不同的 conftest.py作用域不同灵活度很高。比如根目录的 conftest.py 放全局的 driver fixturetest_cases 目录下的 conftest.py 放登录相关的数据加载 fixture。这个机制能避免在每条用例里重复写 setup 代码。2. 核心实战拆解用登录功能跑通第一条用例环境准备好之后最直接的练手场景就是登录。为什么选登录因为登录几乎是所有 Web 系统都有的功能覆盖了元素定位、文本输入、点击按钮、等待跳转、断言反馈这些最基础的 UI 自动化动作而且整个流程直观新手容易对照理解。2.1 环境准备与踩坑预判先说环境准备这部分我踩过的坑比写用例本身多得多。Python 版本建议 3.8 以上然后用 pip 安装依赖pip install selenium pytest pytest-html webdriver-manager pyyaml这里强烈建议直接用 webdriver-manager 来管理 WebDriver而不是手动下载。手动下载最大的问题是浏览器一升级ChromeDriver 就版本不匹配脚本立刻跑挂。webdriver-manager 会自动检测本机浏览器版本并下载对应的 Driver省掉一大半环境问题。看一段实际代码就明白多省事了from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager def create_driver(): driver webdriver.Chrome(ChromeDriverManager().install()) driver.maximize_window() driver.implicitly_wait(10) return driver这只是一个最简单的封装实际项目中我们通常会在 utils/driver_utils.py 里封装不同浏览器的启动方式再配合配置文件选择运行环境。2.2 页面对象层的代码实现按照前面说的分层设计先写 LoginPagefrom 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.username_input (By.ID, username) self.password_input (By.ID, password) self.login_button (By.ID, loginBtn) self.error_tip (By.CLASS_NAME, error-tip) def input_username(self, username): ele WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located(self.username_input) ) ele.clear() ele.send_keys(username) def input_password(self, password): ele self.driver.find_element(*self.password_input) ele.clear() ele.send_keys(password) def click_login(self): self.driver.find_element(*self.login_button).click() def get_error_tip(self): 获取登录失败时的错误提示文本 ele WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located(self.error_tip) ) return ele.text注意看 get_error_tip 方法里的逻辑登录失败后错误提示的渲染是异步的如果直接用 find_element 去拿很可能拿不到元素所以这里用了 WebDriverWait 等待元素可见这是 UI 自动化里非常关键的一个习惯。2.3 数据驱动一条用例覆盖多种登录场景光测试正确用户名正确密码能成功远远不够自动化测试的价值在于快速覆盖大量场景。我们可以用 Pytest 的参数化功能把测试数据抽离出来。先在 data/login_data.yaml 里维护数据cases: - case_id: test_login_success username: test_user password: 123456 expect: success - case_id: test_login_wrong_password username: test_user password: wrong expect: fail - case_id: test_login_empty_username username: password: 123456 expect: fail然后写测试用例import pytest import yaml from pages.login_page import LoginPage def load_login_data(): with open(data/login_data.yaml, encodingutf-8) as f: return yaml.safe_load(f)[cases] pytest.mark.parametrize(case, load_login_data()) def test_login_scenarios(driver, case): login_page LoginPage(driver) driver.get(http://xxxx/demo/login) # 用实际的被测系统地址替换 login_page.input_username(case[username]) login_page.input_password(case[password]) login_page.click_login() if case[expect] success: assert driver.current_url http://xxxx/demo/home # 替换为实际跳转地址 else: error_text login_page.get_error_tip() assert 用户名或密码错误 in error_text这种做法就是数据驱动测试。测试数据和测试代码分离的好处在于当产品新增一种登录异常场景时我们只需要在 YAML 文件里追加一条数据完全不需要改代码。这种模式扩展到商城搜索、订单流程等场景时价值会被放大很多倍。2.4 关于元素定位和等待机制的经验补完在实际写用例时元素定位和等待是两个最影响稳定性的点这里多说几句。关于定位方式优先级一般是 id name class_name css_selector xpath。id 是最稳定的基本不会变优先级最高。xpath 虽然灵活但容易写出非常脆弱的绝对路径表达式页面结构稍微调整就失效。我一般只在没有其他选择时用 xpath而且尽量用相对路径像//button[contains(text(), 登录)]这种写法可读性和稳定性都不错。关于等待机制我这里有一个从实战中总结出的原则能用显式等待就别用隐式等待。隐式等待虽然设置一次全局生效但它对元素不可见、不可点击这类状态判断是无效的而且它和显式等待混用的时候会出现等待时间叠加的问题调大调小都很难把握。显式等待配合 expected_conditions可以精确地等到按钮可点击、元素可见、元素消失这些具体状态每一处等待都明确知道在等什么排查失败时也容易定位问题。3. 工程化进阶让用例跑得更稳、结果更清楚单条用例跑通只是第一步真正让测试框架产生价值的是把公共逻辑抽取出来、执行结果清晰可读、异常时能快速定位。这一节说说我在实际项目中常用的工程化技巧。3.1 conftest.py 里设计 driver fixture为了让所有用例共享同一个浏览器实例同时又在合适的时候关闭它我们可以在 conftest.py 里定义一个 driver fixtureimport pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager pytest.fixture(scopefunction) def driver(): driver webdriver.Chrome(ChromeDriverManager().install()) driver.maximize_window() driver.implicitly_wait(3) yield driver driver.quit()scope 是 fixture 设计中最重要的参数。function 表示每条用例都会重开一个浏览器隔离性好但执行慢class 或 module 表示整个测试类/模块共享一个浏览器执行快但用例之间的状态可能互相干扰。我一般建议默认用 function除非有明确的性能优化需求再考虑提升作用域。fixture 里的 yield 是另一个关键点yield 之前的代码是前置操作yield 之后的代码是后置清理。即使用例中途断言失败yield 后的清理代码也会照常执行不会留下僵尸浏览器进程。3.2 失败截图与日志收集用例失败时一张当时的页面截图比一屏报错日志有用得多。在 conftest.py 里我们可以通过 pytest 的钩子函数在用例失败时自动截图import os import pytest from datetime import datetime 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: report_dir reports os.makedirs(report_dir, exist_okTrue) timestamp datetime.now().strftime(%Y%m%d_%H%M%S) file_name f{report_dir}/failure_{item.name}_{timestamp}.png driver.save_screenshot(file_name) report.extra.append(getattr(__import__(pytest_html), extras).image(file_name)) print(f失败截图已保存: {file_name})这个钩子的逻辑是pytest 在每一条用例执行完成后会生成一个报告对象我们通过 hookwrapper 拿到结果判断是 call 阶段真正执行测试函数体阶段且失败了就从 fixture 里取出 driver 来截图。截图文件名带上用例名和时间戳方便后续追踪。日志这块标准库 logging 就够用。建议在关键操作处打印日志比如输入用户名: test_user、点击登录按钮、断言失败当前URL: xxx。排查问题时配合日志能找到用例到底卡在了哪一步比只看一个元素找不到的异常要直观得多。3.3 生成直观的测试报告pytest-html 是当前最常见的报告插件用法很简单pytest test_cases/ -s -v --htmlreports/report.html --self-contained-html--self-contained-html 参数很重要它会把 CSS、JS 全部内联到 HTML 文件里这样报告文件可以直接发给别人看不会出现样式丢失的问题。在 Jenkins 或 GitLab CI 里这个报告路径还可以配置成构建产物打开网页就能看到每一轮的测试结果。如果想让报告更漂亮、集成截图和性能数据可以选择 Allure 报告它的安装和配置会重一些但展示效果的确更好。从优先级来看pytest-html 足够覆盖大多数团队的需求Allure 可以作为后期优化项。3.4 参数化进阶用 YAML 管理多环境配置很多系统有测试环境、预发布环境之分把环境地址写死在代码里不是好习惯。可以把环境配置也放进 YAML通过命令行参数来指定pytest test_cases/ --envtest --htmlreports/report.html通过 conftest.py 里再加一个 env fixture 来读取配置。这样的设计好处是测试代码本身和环境完全解耦部署不同环境跑测试时只需要改启动命令不需要改代码。这个习惯养成之后你会发现自动化测试框架和被测系统之间的耦合度被降得很低。4. 高频问题排查与稳定性提升策略即使框架结构没问题实际跑起来总会有各种幺蛾子。这里整理了几个我在项目里真实遇到并且反复出现过的问题每一个都付出了实实在在的调试成本。4.1 WebDriver 版本不匹配导致启动失败这是新手最容易遇到的头号问题。表现就是启动浏览器时报session not created或者This version of ChromeDriver only supports Chrome version xx。以前我们需要手动去查浏览器版本号再跑到对应网站下载匹配的 Driver非常麻烦。用 webdriver-manager 可以从根源上解决这个问题它会读取本机浏览器真实版本并自动拉取对应 Driver。注意第一次运行时它会下载 Driver速度取决于网络之后会走本地缓存。还有一个很容易忽略的小细节如果你在 CI 容器里跑测试容器里没有安装浏览器那即使 Driver 正常也会启动失败。这时需要确保 CI 环境预装好 Chrome 或 Edge而不只是把 Python 依赖装好就行。4.2 元素明明存在脚本却找不到这个问题的排查方向比问题本身更值得讲。常见原因有几类按优先级排查第一类是页面还没完全加载元素是异步渲染出来的。解法就是显式等待前面已经说了这里再强调一遍不要依赖 time.sleep 这种硬等待既不稳定又拖慢执行速度。第二类是被 iframe 遮挡。元素在 iframe 里主页面直接 find 是找不到的必须先driver.switch_to.frame()切换到对应 iframe 才能定位操作完成后再切回主文档。遇到找不到元素的情况先查看页面源代码确认是不是 iframe 的锅这一步能排除很大一部分问题。第三类是浏览器窗口缩放导致的元素不可见或遮挡。有些网站在小窗口下会折叠菜单或隐藏按钮元素虽然在 DOM 里但点击时会报元素不可交互。解决办法是启动浏览器时统一设置窗口大小并且在点击前用 expected_conditions 的 element_to_be_clickable 来等待可点击状态。4.3 用例执行顺序导致的相互依赖默认情况下 Pytest 按文件名的字母序执行用例。如果你的用例之间有依赖比如 A 用例创建了数据B 用例需要用到这个数据那一旦执行顺序变化B 就会失败。这个问题在工程里必须通过数据隔离去解决而不是靠排序强撑。我的做法是每条用例尽量使用独立的测试数据或者通过 fixture 在用例前动态创建数据、用例后清理数据。哪怕代码写得复杂一点也比用例之间互相依赖要可控得多。如果真的有些流程必须串联可以使用pytest.mark.dependency或在同一用例里把整个链路跑完不要拆成多条用例。4.4 报告中文乱码问题pytest-html 报告里如果出现中文字符变乱码大部分情况是系统编码问题。在运行 pytest 之前建议先确认终端编码export PYTHONIOENCODINGutf-8Windows 下还可以在 conftest.py 顶部加上# -*- coding: utf-8 -*-并在 pytest.ini 里设置编码。如果你用了 YAML 文件记得指定encodingutf-8去读文件避免 windows 默认编码带来的麻烦。4.5 CI 里跑 UI 自动化总是超时或闪退本地跑得好好的一上 CI 就各种失败这个问题我在多个团队都遇到过。根因通常是资源问题CI 机器配置低浏览器渲染慢显式等待时间给的 10 秒在负载高时可能不够。另外容器环境缺少 GPU 加速浏览器渲染方式不同也会影响元素定位的稳定性。针对这个情况可以做的几件事适当调大等待超时时间使用 headless 模式运行options.add_argument(--headless)降低资源消耗在 CI 配置里加xvfb环境Linux来支持浏览器无头运行。还有一个关键习惯把失败截图和日志作为 CI 构建产物上传这样在查看失败原因时就不用只靠猜了。最后再分享几个我实际干活时的小习惯关于这套自动化框架最后补充几点我自己工作时的经验都是值得刻意养成的习惯。第一个是写用例时习惯性先手动走一遍流程。自动化脚本本质上是把手工操作翻译成代码如果手工操作这个场景本身就是错误的代码再怎么写都是错。所以每次接到新的测试场景我先手动点一遍把路径和预期结果记录清楚再动手写自动化。第二个是时刻关注稳定性优于覆盖率。刚开始搭框架与其追求几十条用例跑完不如先保证 5 条核心用例能连续稳定跑 10 次不挂。UI 自动化最忌讳的是用例不稳定今天过明天挂最后团队失去对自动化结果的信任。一个稳定的 5 条用例框架比一个天天挂的 50 条用例框架有价值得多。第三个是定期回顾元素定位和等待逻辑。前端代码迭代频繁页面结构变化在所难免。建议每隔一个迭代周期专门抽出时间跑一遍全量用例根据失败情况判断是产品需求变化导致的预期更新还是页面结构变化导致的定位失效。这种主动维护的习惯比等自动化大量失败之后再花整块时间修复要轻松得多。这套 Python Selenium Pytest 的组合说实话并不炫酷但它是目前最稳、最通用的 Web UI 自动化测试方案。希望这篇文章里拆解的项目结构、数据驱动思路和稳定性经验能帮你在搭建自己的框架时少走一些弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑