4. 页面对象夹具封装:BasePage、组件对象、行为抽象
页面对象模型(Page Object Model, POM)是 UI 自动化测试中最经典的夹具组织模式。但在实际工程中,大量重复的 find_element、等待逻辑、通用操作方法依然散落在各个 Page 类中,形成隐性冗余。本章将以 **BasePage 基类 + 组件对象 + 行为抽象** 三层结构为核心,将夹具封装提升为可复用、可维护的工程化方案。
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 并调用
组件对象继承 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 或等待策略时,只修改基类一处,全工程生效。