Skip to content

一、传统线性脚本的工程痛点 ​

很多测试工程师在初学 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()

这种脚本随规模扩大会引发严重的维护雪崩: ​

  1. 元素定位散落各处:一旦前端修改了一个输入框 ID 或按钮样式,几十个测试文件需要逐一查找修改;
  2. time.sleep 的两难陷阱:时间设短了遇到网络卡顿时偶发失败(Flaky Tests),时间设长了导致千条用例执行耗时从半小时膨胀到几小时;
  3. 职责不分:用例代码既在管浏览器底层细节,又在管业务流程,缺乏分层复用能力。

二、页面对象模型(Page Object Model)核心思想 ​

POM 是 Martin Fowler 提出的经典 UI 自动化设计范式,其核心原则是:

  1. 页面对象(Page Object)代表界面的一个可复用单元,对外暴露该页面能够提供的业务操作方法(如 login(), search_order()),隐藏底层的元素定位选择器;
  2. Page Object 内部不写任何测试断言,只负责页面交互和状态流转(返回自身或下一个目标页面);
  3. 测试用例(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 这一处常量,整个测试工程的维护成本呈指数级下降。

测试开发工程师 · 专注自动化与系统架构 | 邮箱: hansblog@atumsoul.win