设计模式:页面对象(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_username、enter_password和click_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对象可在多个测试中重用
- 页面操作方法可被多个测试场景调用
- 减少重复代码,提高开发效率
✅ 改善可读性
- 测试代码更贴近业务语言
- 隐藏技术实现细节
- 提高团队协作效率
缺点
❌ 学习成本
- 需要理解设计模式概念
- 需要额外的代码设计和规划
❌ 初期开发量
- 需要编写页面对象类
- 前期代码量相对较大
❌ 过度设计风险
- 简单页面可能过度封装
- 需要合理权衡复杂度
最佳实践建议
- 合理分层:严格按照三层结构组织代码
- 统一命名:使用一致的命名规范
- 适度封装:避免过度设计,保持简洁
- 持续重构:随着项目发展不断优化结构
- 文档完善:为页面对象类添加清晰的文档说明