跳转至

设计模式:页面对象(PO)模式详解

页面对象模型(Page Object Model, POM)是UI自动化测试领域最重要、最广泛使用的设计模式。

核心思想

POM的核心思想是分离测试逻辑与UI交互逻辑。它将每一个页面视为一个独立的对象,该对象封装了所有与该页面相关的元素定位符和操作方法。测试用例本身则只负责调用这些页面对象的方法来执行业务流程,而不直接与Selenium的API打交道。

设计原则

  • 单一职责:每个页面类只负责一个页面的操作
  • 封装性:隐藏页面实现细节,只暴露必要的接口
  • 复用性:页面对象可以在多个测试中重复使用

传统测试代码的问题

问题示例:重复且难维护的代码

Python
# 传统写法 - 问题重重
def test_login_success():
    driver = webdriver.Chrome()
    driver.get("https://example.com/login")

    # 大量重复的元素定位代码
    driver.find_element(By.ID, "username").send_keys("testuser")
    driver.find_element(By.ID, "password").send_keys("testpass")
    driver.find_element(By.ID, "login-btn").click()

    assert "dashboard" in driver.current_url
    driver.quit()

def test_login_failure():
    driver = webdriver.Chrome()
    driver.get("https://example.com/login")

    # 相同的元素定位代码再次出现
    driver.find_element(By.ID, "username").send_keys("wronguser")
    driver.find_element(By.ID, "password").send_keys("wrongpass")
    driver.find_element(By.ID, "login-btn").click()

    error = driver.find_element(By.CLASS_NAME, "error-message")
    assert error.is_displayed()
    driver.quit()

传统方法的痛点

  • 代码重复:相同的元素定位代码在多个测试中重复出现
  • 维护困难:页面元素变化时需要修改所有相关测试
  • 可读性差:测试逻辑被大量技术细节掩盖
  • 脆弱性高:任何页面变化都可能导致多个测试失败

三层结构

一个设计良好的PO模式通常包含三个层次,实现关注点分离:

1. 对象库层 (Locators/Object Repository)

这一层只做一件事——以变量的形式存储页面的所有元素定位符。例如,login_button = (By.ID, "login")。当UI元素的定位方式改变时(如ID从"login"变为"submit"),只需修改这一层的一个变量即可。

Python
# login_locators.py
from selenium.webdriver.common.by import By

class LoginPageLocators:
    """登录页面元素定位符"""
    USERNAME_INPUT = (By.ID, "username")
    PASSWORD_INPUT = (By.ID, "password")
    LOGIN_BUTTON = (By.ID, "login-btn")
    ERROR_MESSAGE = (By.CLASS_NAME, "error-message")
    REMEMBER_CHECKBOX = (By.ID, "remember-me")

2. 操作层 (Operations)

这一层封装了对页面元素的具体原子操作。它会导入对象库层的定位符,并定义一系列方法,如enter_username(username)click_login_button()等。这些方法内部调用Selenium的find_element和click、send_keys等API。

Python
# login_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from .login_locators import LoginPageLocators

class LoginPage:
    """登录页面操作类"""

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)

    def enter_username(self, username):
        """输入用户名"""
        element = self.wait.until(EC.presence_of_element_located(LoginPageLocators.USERNAME_INPUT))
        element.clear()
        element.send_keys(username)

    def enter_password(self, password):
        """输入密码"""
        element = self.driver.find_element(*LoginPageLocators.PASSWORD_INPUT)
        element.clear()
        element.send_keys(password)

    def click_login_button(self):
        """点击登录按钮"""
        element = self.wait.until(EC.element_to_be_clickable(LoginPageLocators.LOGIN_BUTTON))
        element.click()

    def is_error_message_displayed(self):
        """检查错误消息是否显示"""
        try:
            element = self.driver.find_element(*LoginPageLocators.ERROR_MESSAGE)
            return element.is_displayed()
        except:
            return False

    def check_remember_me(self):
        """勾选记住我复选框"""
        element = self.driver.find_element(*LoginPageLocators.REMEMBER_CHECKBOX)
        if not element.is_selected():
            element.click()

3. 业务层 (Business/Proxy)

这一层代表了实际的业务流程。它会调用操作层的多个原子操作,将其组合成一个完整的业务功能。例如,一个login(username, password)方法,其内部会依次调用enter_usernameenter_passwordclick_login_button

Python
# login_business.py
from .login_page import LoginPage

class LoginBusiness:
    """登录业务逻辑类"""

    def __init__(self, driver):
        self.driver = driver
        self.login_page = LoginPage(driver)

    def login_with_credentials(self, username, password, remember_me=False):
        """使用凭据登录"""
        self.login_page.enter_username(username)
        self.login_page.enter_password(password)

        if remember_me:
            self.login_page.check_remember_me()

        self.login_page.click_login_button()

    def login_with_valid_credentials(self):
        """使用有效凭据登录"""
        self.login_with_credentials("admin", "admin123")

    def login_with_invalid_credentials(self):
        """使用无效凭据登录"""
        self.login_with_credentials("wrong", "wrong")

    def verify_login_failure(self):
        """验证登录失败"""
        return self.login_page.is_error_message_displayed()

PO模式的巨大优势

采用PO模式的根本原因在于应对UI的频繁变化。在没有PO模式的情况下,如果一个登录按钮的ID改变了,你可能需要修改所有涉及到登录操作的测试脚本。而在PO模式下,你只需要去登录页面的对象库层修改那一个定位符变量,所有调用该元素的测试用例都会自动更新。这极大地降低了维护成本,并提高了代码的复用性和可读性。

维护性提升示例

Python
# 如果登录按钮ID从 "login-btn" 改为 "submit-button"
# 传统方式:需要修改所有使用该元素的测试代码
# PO模式:只需修改LoginPageLocators类中的LOGIN_BUTTON定位器
class LoginPageLocators:
    LOGIN_BUTTON = (By.ID, "submit-button")  # 只改这一处

复用性增强示例

Python
# 同一个LoginBusiness可以在多个测试类中使用
class TestUserManagement(unittest.TestCase):
    def test_user_profile_after_login(self):
        login_business = LoginBusiness(self.driver)
        login_business.login_with_valid_credentials()
        # 继续测试用户资料功能

class TestOrderManagement(unittest.TestCase):
    def test_order_creation_after_login(self):
        login_business = LoginBusiness(self.driver)
        login_business.login_with_valid_credentials()
        # 继续测试订单功能

可读性改善示例

Python
# 业务语言化的测试代码
def test_business_scenario(self):
    # 清晰的业务流程
    login_business.login_with_valid_credentials()
    dashboard_business.navigate_to_orders()
    order_business.create_new_order()

    # 而不是技术细节
    # driver.find_element(By.ID, "username").send_keys("test")
    # driver.find_element(By.ID, "password").send_keys("pass")
    # driver.find_element(By.ID, "login").click()

完整的测试示例

Python
# test_login_with_po.py
import unittest
from selenium import webdriver
from pages.login_business import LoginBusiness

class TestLoginWithPO(unittest.TestCase):

    def setUp(self):
        self.driver = webdriver.Chrome()
        self.driver.get("https://example.com/login")
        self.login_business = LoginBusiness(self.driver)

    def tearDown(self):
        self.driver.quit()

    def test_successful_login(self):
        """测试成功登录"""
        # 业务语言,清晰易懂
        self.login_business.login_with_valid_credentials()

        # 验证登录成功
        self.assertIn("dashboard", self.driver.current_url)

    def test_failed_login(self):
        """测试登录失败"""
        # 使用无效凭据登录
        self.login_business.login_with_invalid_credentials()

        # 验证显示错误信息
        self.assertTrue(self.login_business.verify_login_failure())

    def test_remember_me_login(self):
        """测试记住我功能"""
        self.login_business.login_with_credentials("admin", "admin123", remember_me=True)
        self.assertIn("dashboard", self.driver.current_url)

if __name__ == '__main__':
    unittest.main()

PO模式的优缺点

优点

✅ 分离关注点

  • 页面结构:在Page类中定义
  • 页面操作:在Page类方法中实现
  • 测试逻辑:在测试类中专注业务逻辑

✅ 提高可维护性

  • 页面元素变化只需修改一个地方
  • 新增页面功能只需扩展Page类
  • 测试代码更稳定,不受页面技术细节影响

✅ 增强可重用性

  • Page对象可在多个测试中重用
  • 页面操作方法可被多个测试场景调用
  • 减少重复代码,提高开发效率

✅ 改善可读性

  • 测试代码更贴近业务语言
  • 隐藏技术实现细节
  • 提高团队协作效率

缺点

❌ 学习成本

  • 需要理解设计模式概念
  • 需要额外的代码设计和规划

❌ 初期开发量

  • 需要编写页面对象类
  • 前期代码量相对较大

❌ 过度设计风险

  • 简单页面可能过度封装
  • 需要合理权衡复杂度

最佳实践建议

  1. 合理分层:严格按照三层结构组织代码
  2. 统一命名:使用一致的命名规范
  3. 适度封装:避免过度设计,保持简洁
  4. 持续重构:随着项目发展不断优化结构
  5. 文档完善:为页面对象类添加清晰的文档说明