一、传统线性脚本的工程痛点
很多测试工程师在初学 Selenium 或 Playwright 时,习惯将页面元素定位、操作逻辑与断言混合平铺在测试用例中:
python
# 🔴 典型的脆弱线性脚本反模式
def test_login_flow():
driver = webdriver.Chrome()
driver.get("https://portal.example.com")
time.sleep(3) # 脆弱强制等待:快了报错,慢了浪费时间
driver.find_element(By.ID, "username").send_keys("admin")
driver.find_element(By.ID, "password").send_keys("123456")
driver.find_element(By.XPATH, "//button[@type='submit']").click()
time.sleep(5)
assert "仪表盘" in driver.page_source
driver.quit()这种脚本随规模扩大会引发严重的维护雪崩:
- 元素定位散落各处:一旦前端修改了一个输入框 ID 或按钮样式,几十个测试文件需要逐一查找修改;
time.sleep的两难陷阱:时间设短了遇到网络卡顿时偶发失败(Flaky Tests),时间设长了导致千条用例执行耗时从半小时膨胀到几小时;- 职责不分:用例代码既在管浏览器底层细节,又在管业务流程,缺乏分层复用能力。
二、页面对象模型(Page Object Model)核心思想
POM 是 Martin Fowler 提出的经典 UI 自动化设计范式,其核心原则是:
- 页面对象(Page Object)代表界面的一个可复用单元,对外暴露该页面能够提供的业务操作方法(如
login(),search_order()),隐藏底层的元素定位选择器; - Page Object 内部不写任何测试断言,只负责页面交互和状态流转(返回自身或下一个目标页面);
- 测试用例(Test Case)只负责场景组装与最终业务断言。
text
┌────────────────────────────────────────────────────────┐
│ Test Case 层 (测试用例与断言) │
│ • def test_user_login(): │
│ dashboard = LoginPage(driver).login("admin") │
│ assert dashboard.get_welcome_text() == "欢迎" │
└───────────────────────────▲────────────────────────────┘
│ 调用业务方法
┌───────────────────────────▼────────────────────────────┐
│ Page Object 层 (页面对象与操作封装) │
│ • LoginPage: fill_credentials(), submit_form() │
│ • DashboardPage: get_welcome_text() │
└───────────────────────────▲────────────────────────────┘
│ 继承基础设施
┌───────────────────────────▼────────────────────────────┐
│ BasePage 层 (显式等待底层封装) │
│ • wait_clickable(), find(), safe_click() │
└────────────────────────────────────────────────────────┘三、三种等待机制的本质辨析
| 等待类型 | 实现机制 | 优缺点分析 | 推荐度 |
|---|---|---|---|
强制等待(time.sleep) | Python 线程死等 | 必须严禁滥用。毫无智能性,极大拉长构建耗时 | 🔴 仅用于临时本地调试 |
隐式等待(implicitly_wait) | 全局轮询查找 DOM 树 | 只能判断元素是否在 DOM 中,无法判断元素是否可见、可点击或已禁用 | 🟡 适合作为基础兜底 |
显式等待(WebDriverWait) | 针对特定元素条件轮询 | 业界黄金标准。精确等待元素变为 Visible、Clickable,条件满足立即返回,超时才抛异常 | 🟢 强烈推荐首选 |
四、工程级 BasePage 封装实现
封装统一的基础页面类,为所有子页面提供健壮的显式等待保护:
python
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
class BasePage:
"""所有页面对象的基类"""
def __init__(self, driver: WebDriver, timeout: int = 10):
self.driver = driver
self.wait = WebDriverWait(driver, timeout, poll_frequency=0.5)
def find_visible_element(self, locator: tuple):
"""等待元素在页面上物理可见"""
try:
return self.wait.until(EC.visibility_of_element_located(locator))
except TimeoutException:
raise TimeoutException(f"元素在 {self.wait._timeout} 秒内未变为可见状态: {locator}")
def safe_click(self, locator: tuple):
"""等待元素可被点击并触发操作"""
element = self.wait.until(EC.element_to_be_clickable(locator))
element.click()
return self # 支持链式调用
def safe_input(self, locator: tuple, text: str):
"""清空并键入内容"""
element = self.find_visible_element(locator)
element.clear()
element.send_keys(text)
return self五、业务页面流转与断言用例示范
1. 业务页面对象实现(LoginPage)
python
from selenium.webdriver.common.by import By
from .base_page import BasePage
from .dashboard_page import DashboardPage
class LoginPage(BasePage):
# 选择器统一定义在类顶部作为静态常量
INPUT_USERNAME = (By.ID, "username")
INPUT_PASSWORD = (By.ID, "password")
BTN_SUBMIT = (By.XPATH, "//button[@type='submit']")
def open(self, url: str):
self.driver.get(url)
return self
def login_as(self, username: str, password: str) -> DashboardPage:
"""输入凭据并完成提交,方法返回流转后的目标页面对象"""
self.safe_input(self.INPUT_USERNAME, username)
self.safe_input(self.INPUT_PASSWORD, password)
self.safe_click(self.BTN_SUBMIT)
# 页面流转:返回主页对象供下一步链式断言
return DashboardPage(self.driver)2. 最终测试用例书写(纯粹、简洁且自解释)
python
def test_admin_login_success(driver):
# 用例逻辑直观清晰,完全与底层定位器解耦
dashboard = LoginPage(driver)\
.open("https://portal.example.com")\
.login_as(username="admin_user", password="SecurePassword123")
assert dashboard.get_profile_name() == "系统管理员"即便前端明天把登录按钮的 XPath 改掉,全工程也只需修改 LoginPage 的 BTN_SUBMIT 这一处常量,整个测试工程的维护成本呈指数级下降。