4. 页面对象夹具封装:BasePage、组件对象、行为抽象

页面对象模型(Page Object Model, POM)是 UI 自动化测试中最经典的夹具组织模式。但在实际工程中,大量重复的 find_element、等待逻辑、通用操作方法依然散落在各个 Page 类中,形成隐性冗余。本章将以 **BasePage 基类 + 组件对象 + 行为抽象** 三层结构为核心,将夹具封装提升为可复用、可维护的工程化方案。

页面对象夹具封装三层架构 页面对象夹具封装三层架构 BasePage 基类 driver / wait / 通用定位 / 截图 组件对象(Component Object) Header / Footer / SearchBar / Table 等独立 UI 部件 行为抽象(Action Abstraction) click_and_wait / type_submit / drag_drop_combine 所有夹具代码集中在三层中,消除散落重复,降低维护成本

4.1 原理:为什么需要三层分离?

传统 POM 中,每个 Page 类都会重复实现 find_element_by_id、等待逻辑、截图等方法。当项目包含 50+ 页面时,这些重复代码累计可达数千行,且一旦底层 Selenium API 变化(如升级到 4.x),需要逐一修改。

三层分离的核心思路:

  • BasePage 基类:封装所有页面共用的基础设施(driver 管理、显式等待、滚动、截图、日志)。
  • 组件对象:将页面中可复用的 UI 部件(如导航栏、搜索框、分页器)抽象为独立类,可在多个 Page 中组合使用。
  • 行为抽象:将常见操作-等待-验证链条(如点击后等待元素可见)打包为函数,避免每个 Page 中重复编写。

4.2 BasePage 基类实现

基类应包含 driver 引用、显式等待对象、通用元素操作方法、截图与日志。

class BasePage:
    def __init__(self, driver: WebDriver, timeout=10):
        self.driver = driver
        self.wait = WebDriverWait(driver, timeout)
        self.logger = logging.getLogger(self.__class__.__name__)

    def find(self, locator: tuple) -> WebElement:
        """统一定位,失败时截图"""
        try:
            return self.wait.until(EC.presence_of_element_located(locator))
        except TimeoutException:
            self._screenshot(f"find_fail_{locator[1]}")
            raise

    def click(self, locator: tuple) -> None:
        """点击前确保元素可交互"""
        elem = self.wait.until(EC.element_to_be_clickable(locator))
        elem.click()

    def _screenshot(self, name: str) -> None:
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        path = f"screenshots/{name}_{timestamp}.png"
        self.driver.save_screenshot(path)
        self.logger.error("Screenshot saved: %s", path)

4.3 组件对象封装

识别页面中的高频复用区域,例如所有页面顶部都有搜索栏,则提取为 SearchBar 组件。组件对象接收 driver 与父元素定位,提供自己的业务方法。

class SearchBar(BasePage):
    def __init__(self, driver: WebDriver, root_locator: tuple = (By.CSS_SELECTOR, ".search-bar")):
        super().__init__(driver)
        self.root = self.find(root_locator)

    def search(self, keyword: str) -> None:
        input_el = self.root.find_element(By.CSS_SELECTOR, "input[type='text']")
        input_el.clear()
        input_el.send_keys(keyword)
        self.root.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    def get_suggestions(self) -> list[str]:
        items = self.root.find_elements(By.CSS_SELECTOR, ".suggestion-item")
        return [item.text for item in items]

在多个页面类中组合组件:

class HomePage(BasePage):
    def __init__(self, driver):
        super().__init__(driver)
        self.search_bar = SearchBar(driver)

    def search_product(self, product):
        self.search_bar.search(product)

4.4 行为抽象:告别重复的操作链

将“点击元素后等待特定状态”这类高频模式提炼为独立函数,可通过参数化应对不同场景。

def click_and_wait(driver: WebDriver, click_locator: tuple, wait_locator: tuple, timeout=10):
    """点击 click_locator 后等待 wait_locator 可见"""
    element = WebDriverWait(driver, timeout).until(EC.element_to_be_clickable(click_locator))
    element.click()
    WebDriverWait(driver, timeout).until(EC.visibility_of_element_located(wait_locator))

def type_submit(driver: WebDriver, input_locator: tuple, text: str, submit_locator: tuple = None):
    """在输入框键入文本后提交(支持回车或点击提交按钮)"""
    input_elem = WebDriverWait(driver, 10).until(EC.presence_of_element_located(input_locator))
    input_elem.clear()
    input_elem.send_keys(text)
    if submit_locator:
        driver.find_element(*submit_locator).click()
    else:
        input_elem.send_keys(Keys.RETURN)

使用场景对比:

传统写法抽象后
每次重复编写等待、定位、截图一行调用 click_and_wait(driver, loc1, loc2)
每次处理输入后提交细节统一交给 type_submit 处理
异常处理代码分散行为函数内部统一捕获并截图

4.5 调试与常见错误

常见错误 1:组件对象中重复初始化 BasePage
组件对象继承 BasePage 并调用 super().__init__(driver),但若组件内部又创建新的 WebDriverWait,会导致等待配置不一致。应始终复用基类中的 self.wait。
调试技巧: 在 BasePage 的 find 方法中添加日志输出定位符和耗时,能快速定位哪个组件定位失败。将日志级别设为 DEBUG 时,可打印完整调用栈。

4.6 性能与稳定性优化

  • 懒加载组件对象:在 __init__ 中不立即初始化所有组件,而是通过 @property 按需创建,减少构造开销。
  • 缓存定位结果:对于静态元素(如页头 logo),可在组件对象中缓存 WebElement 引用,避免重复查找。
  • 行为抽象超时参数化:为每个行为函数增加默认超时,并允许测试用例重写,避免硬编码。
class LazySearchBar:
    def __init__(self, driver):
        self.driver = driver
        self._input = None

    @property
    def input_el(self):
        if not self._input:
            self._input = WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.ID, "search-input"))
            )
        return self._input

    def search(self, keyword):
        self.input_el.clear()
        self.input_el.send_keys(keyword + "\n")

4.7 部署集成

将三层独立打包为 Python 包,通过 setup.py 或 pyproject.toml 管理依赖:

[tool.poetry.dependencies]
python = "^3.8"
selenium = "^4.15"
pytest = "^7.4"

[tool.poetry.group.test.dependencies]
pytest-selenium = "^4.0"
allure-pytest = "^2.13"

在 CI 中安装后,测试用例仅需导入 BasePage、组件和行为函数,无需关心底层实现。更新 Selenium 或等待策略时,只修改基类一处,全工程生效。