Selenium结合pytest构建自动化测试框架:用例管理与断言
本文介绍如何利用 pytest 的 fixture、参数化机制以及 Selenium Manager,将零散的 Selenium 脚本重构为结构清晰、易于维护的自动化测试工程。通过显式等待和 Page Object 模式分离页面交互与业务断言,提升测试稳定性与可读性。
将 Selenium 脚本纳入 pytest 管理体系的核心,并非简单地将操作封装为函数,而是明确划分浏览器生命周期、页面交互逻辑与业务断言的职责边界。若登录步骤、元素定位器及断言逻辑散落在测试函数中,一旦页面改版,大量用例会变得脆弱难护。下文将以登录页为例,演示如何合理拆分代码,其中涉及的元素 ID、提示文案及登录路径均视为需根据实际应用替换的约定配置。
先把零散脚本拆成稳定的项目边界
最基础的脚本往往将启动浏览器、访问页面、输入凭证、点击按钮及验证结果全部堆砌在一个函数内。虽然代码简短,但涉及重复登录流程的用例会导致定位器冗余复制,且若断言失败,浏览器的清理退出逻辑极易被遗漏。pytest 提供的测试发现、fixture 机制及参数化功能,恰好适合将这些职责解耦。
初期项目可采用如下目录结构进行组织:
selenium_tests/
├── pages/
│ └── login_page.py
├── tests/
│ └── test_login.py
├── conftest.py
└── pytest.ini
tests/用于存放测试用例,pages/负责封装页面交互逻辑,conftest.py则提供共享的 fixture。在项目起步阶段,无需过早引入复杂的基类或插件;待重复代码增多且维护痛点显现时,再逐步抽取公共层。

把测试、页面对象与共享fixture分开放置
Selenium 4 的 Python 绑定内置了 Selenium Manager,可自动管理匹配的浏览器驱动,因此基础项目通常可直接从 webdriver.Chrome() 开始,无需手动下载驱动。前提是本地已安装对应浏览器,且首次解析驱动时网络通畅;若 CI 环境无法联网或需使用预装驱动,则需根据环境特定配置驱动路径。
浏览器的创建与清理应交由 fixture 管理。将作用域设为 function,确保每条测试用例拥有独立的 driver 实例,从而避免 Cookie、窗口状态及页面数据相互污染。yield 之后的清理逻辑具备强制性,即使测试断言失败也会执行:
# conftest.py
import pytest
from selenium import webdriver
@pytest.fixture
def driver():
browser = webdriver.Chrome()
try:
yield browser
finally:
browser.quit()
尽管 pytest 默认 fixture 作用域即为函数级,此处显式声明有助于团队成员直观理解浏览器实例不会跨用例共享。若为了节省启动时间将浏览器提升至 session 级,必须妥善处理登录态、测试数据隔离及失败后的残留状态,否则性能提升可能以引入顺序相关的偶发失败为代价。
用显式等待控制异步页面,用页面对象集中改动
WebDriver 执行点击后,页面的请求与渲染可能仍在进行中。使用固定的 sleep(2) 仅是对页面加载完成的猜测:环境较快时造成无谓等待,环境较慢时仍可能因读取过早而失败。显式等待通过轮询明确条件,满足即继续,超时才报错。应避免混用隐式等待与显式等待,以防等待时长不可控。
传统写法常将等待逻辑与断言混杂在测试代码中:
import time
time.sleep(2)
message = driver.find_element(By.ID, "message").text
assert message == expected
若提示消息尚未渲染完成,元素查找便会失败;单纯延长固定等待时间只能掩盖竞态条件,并拖慢所有成功用例的执行速度。改为针对可见状态的显式等待,可确保测试仅在真正需要检查的页面状态就绪后继续执行。
页面对象应仅封装页面操作与数据读取,业务断言仍需保留在测试函数中。以下 username、password、login、message 均为示例应用中的 HTML 元素 ID,实际使用时需根据被测页面的具体属性进行调整:
# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
class LoginPage:
def __init__(self, driver):
self.driver = driver
def login(self, username, password):
self.driver.find_element(By.ID, "username").send_keys(username)
self.driver.find_element(By.ID, "password").send_keys(password)
self.driver.find_element(By.ID, "login").click()
def message_text(self):
message = WebDriverWait(self.driver, 10).until(
EC.visibility_of_element_located((By.ID, "message"))
)
return message.text
将等待逻辑置于 message_text() 中,是因为该操作描述了读取页面消息前需满足的状态条件;若目标页面通过 URL 跳转或其他标志表示加载完成,则应将等待条件调整为对应状态。Page Object Model 的价值在于集中管理定位器与页面交互,当页面结构变更时,通常只需修改对应的页面对象。切勿在页面对象中吞掉测试断言,否则会导致失败位置模糊,难以辨识业务意图。

fixture负责浏览器清理,显式等待关注页面状态
用参数化管理用例,用测试函数表达断言
将每组输入数据与预期结果拆分为独立用例,可使 pytest 分别报告登录成功、失败等不同场景的结果。页面对象负责执行填写与提交操作,测试函数负责验证结果。其中的账号、错误消息及 TEST_LOGIN_URL 均为示例约定,接入实际测试环境时需替换为真实数据和地址:
# tests/test_login.py
import os
import pytest
from pages.login_page import LoginPage
@pytest.mark.parametrize(
"username, password, expected",
[
("alice", "valid-password", "欢迎,alice"),
("guest", "wrong-password", "用户名或密码错误"),
],
)
def test_login(driver, username, password, expected):
driver.get(os.environ["TEST_LOGIN_URL"])
page = LoginPage(driver)
page.login(username, password)
assert page.message_text() == expected
@pytest.mark.parametrize 会为每组参数生成独立的测试项;fixture 由测试参数 driver 请求,pytest 会在运行用例时自动创建并注入。当断言失败时,pytest 能精准报告具体的参数化用例,相比在单段脚本中循环多组数据,排查问题更为直接。测试环境的地址与凭据应通过环境变量或 CI 密钥注入,严禁将真实账号密码写入代码库。
保持运行入口的简洁性:
python -m pip install selenium pytest
python -m pytest
命令需在已激活的 Python 虚拟环境中执行,依赖版本应通过项目的依赖管理工具固定。若仅需运行特定文件,可将最后一行修改为 python -m pytest tests/test_login.py。如需更精细的失败定位,可后续增加日志、截图或 pytest 插件,无需在初版中就堆叠复杂的报告系统。

参数化复用输入数据,断言留在测试函数
是否引入 Page Object 取决于页面交互的复用程度。若仅有一两个简短用例,直接写在测试中可能更清晰;当页面定位器大量重复、多个测试共享同一业务操作时,抽取页面对象更具性价比。维护自动化测试的核心在于确保用例独立性、等待条件对应真实页面状态,并将断言保留在能清晰表达业务预期的位置。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









