☰
Python+Selenium+Pytest:Web UI自动化测试框架实战
2026/10/18 13:09:15 网站建设 项目流程

想用 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 fixture,test_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 页面对象层的代码实现

按照前面说的分层设计,先写 LoginPage:

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.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", encoding="utf-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 fixture:

import pytest from selenium import webdriver from webdriver_manager.chrome import ChromeDriverManager @pytest.fixture(scope="function") 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(tryfirst=True, 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: report_dir = "reports" os.makedirs(report_dir, exist_ok=True) 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 --html=reports/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/ --env=test --html=reports/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 PYTHONIOENCODING=utf-8

Windows 下还可以在 conftest.py 顶部加上# -*- coding: utf-8 -*-,并在 pytest.ini 里设置编码。如果你用了 YAML 文件,记得指定encoding="utf-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 自动化测试方案。希望这篇文章里拆解的项目结构、数据驱动思路和稳定性经验,能帮你在搭建自己的框架时少走一些弯路。

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

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

立即咨询