Selenium 自动化测试(十):Page Object Model 设计模式
本篇深入讲解测试框架设计的核心模式——Page Object Model(POM),让你的测试代码可维护、可复用、易扩展。
一、为什么需要 POM?
1.1 痛点:没有 POM 的测试代码
# 不使用 POM:测试代码和页面细节混在一起
def test_login():
driver = webdriver.Chrome()
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys("admin")
driver.find_element(By.ID, "password").send_keys("123456")
driver.find_element(By.CSS_SELECTOR, "#login-form button").click()
assert "欢迎" in driver.find_element(By.CSS_SELECTOR, ".user-info span").text
driver.quit()
def test_login_with_wrong_password():
driver = webdriver.Chrome()
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys("admin")
driver.find_element(By.ID, "password").send_keys("wrong")
driver.find_element(By.CSS_SELECTOR, "#login-form button").click()
assert "密码错误" in driver.find_element(By.CSS_SELECTOR, ".error-msg").text
driver.quit()
问题:
- 元素定位器重复出现在多个测试中
- 页面结构变更需要修改所有相关测试
- 代码难以维护和复用
1.2 POM 的核心思想
将页面元素定位和操作封装到独立的 Page 类中
测试代码只调用 Page 类的方法,不直接操作元素
二、POM 基础实现
2.1 Page 基类
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
"""所有 Page 类的基类"""
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def find_element(self, locator):
"""等待元素可见并返回"""
return self.wait.until(
EC.visibility_of_element_located(locator)
)
def find_elements(self, locator):
"""等待元素出现并返回列表"""
return self.wait.until(
EC.presence_of_all_elements_located(locator)
)
def click(self, locator):
"""等待元素可点击并点击"""
self.wait.until(
EC.element_to_be_clickable(locator)
).click()
def input_text(self, locator, text):
"""输入文本"""
element = self.find_element(locator)
element.clear()
element.send_keys(text)
def get_text(self, locator):
"""获取元素文本"""
return self.find_element(locator).text
def is_displayed(self, locator):
"""判断元素是否可见"""
try:
return self.find_element(locator).is_displayed()
except:
return False
def wait_for_url_contains(self, keyword):
"""等待 URL 包含关键词"""
self.wait.until(EC.url_contains(keyword))
def wait_for_title_contains(self, keyword):
"""等待标题包含关键词"""
self.wait.until(EC.title_contains(keyword))
2.2 登录页面 Page 类
from selenium.webdriver.common.by import By
class LoginPage(BasePage):
"""登录页面"""
# URL
URL = "https://example.com/login"
# 元素定位器
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.CSS_SELECTOR, "#login-form button")
ERROR_MESSAGE = (By.CSS_SELECTOR, ".error-msg")
FORGOT_PASSWORD_LINK = (By.LINK_TEXT, "忘记密码?")
REGISTER_LINK = (By.LINK_TEXT, "注册账号")
def __init__(self, driver):
super().__init__(driver)
def open(self):
"""打开登录页面"""
self.driver.get(self.URL)
return self
def login(self, username, password):
"""执行登录操作"""
self.input_text(self.USERNAME_INPUT, username)
self.input_text(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
return self # 支持链式调用
def get_error_message(self):
"""获取错误提示信息"""
return self.get_text(self.ERROR_MESSAGE)
def go_to_forgot_password(self):
"""跳转到忘记密码页面"""
self.click(self.FORGOT_PASSWORD_LINK)
return ForgotPasswordPage(self.driver)
def go_to_register(self):
"""跳转到注册页面"""
self.click(self.REGISTER_LINK)
return RegisterPage(self.driver)
2.3 主页 Page 类
class HomePage(BasePage):
"""登录后的主页"""
# 元素定位器
USER_AVATAR = (By.CSS_SELECTOR, ".user-avatar")
USER_NAME = (By.CSS_SELECTOR, ".user-info span")
LOGOUT_BUTTON = (By.CSS_SELECTOR, ".logout-btn")
NAV_MENU = (By.CSS_SELECTOR, ".nav-menu")
SEARCH_INPUT = (By.CSS_SELECTOR, ".search-box input")
def get_user_name(self):
"""获取当前登录用户名"""
return self.get_text(self.USER_NAME)
def is_logged_in(self):
"""判断是否已登录"""
return self.is_displayed(self.USER_AVATAR)
def logout(self):
"""退出登录"""
self.click(self.USER_AVATAR)
self.click(self.LOGOUT_BUTTON)
return LoginPage(self.driver)
def search(self, keyword):
"""搜索"""
self.input_text(self.SEARCH_INPUT, keyword)
from selenium.webdriver.common.keys import Keys
self.find_element(self.SEARCH_INPUT).send_keys(Keys.RETURN)
return SearchResultsPage(self.driver)
2.4 使用 POM 编写测试
def test_login_success():
driver = webdriver.Chrome()
try:
login_page = LoginPage(driver).open()
login_page.login("admin", "123456")
home_page = HomePage(driver)
assert home_page.is_logged_in()
assert home_page.get_user_name() == "admin"
finally:
driver.quit()
def test_login_with_wrong_password():
driver = webdriver.Chrome()
try:
login_page = LoginPage(driver).open()
login_page.login("admin", "wrong_password")
assert "密码错误" in login_page.get_error_message()
finally:
driver.quit()
def test_login_logout_flow():
driver = webdriver.Chrome()
try:
login_page = LoginPage(driver).open()
login_page.login("admin", "123456")
home_page = HomePage(driver)
login_page = home_page.logout()
assert not HomePage(driver).is_logged_in()
finally:
driver.quit()
三、POM 进阶设计
3.1 组件化设计(Page Components)
当页面包含多个独立区块时,可以拆分为组件:
class NavigationBar(BasePage):
"""导航栏组件"""
LOGO = (By.CSS_SELECTOR, ".logo")
SEARCH_INPUT = (By.CSS_SELECTOR, ".nav-search input")
USER_MENU = (By.CSS_SELECTOR, ".user-menu")
NOTIFICATION_BELL = (By.CSS_SELECTOR, ".notification-bell")
def search(self, keyword):
self.input_text(self.SEARCH_INPUT, keyword)
from selenium.webdriver.common.keys import Keys
self.find_element(self.SEARCH_INPUT).send_keys(Keys.RETURN)
def click_user_menu(self):
self.click(self.USER_MENU)
def get_notification_count(self):
element = self.driver.find_element(self.NOTIFICATION_BELL)
badge = element.find_element(By.CSS_SELECTOR, ".badge")
return int(badge.text)
class Sidebar(BasePage):
"""侧边栏组件"""
def click_menu_item(self, menu_name):
locator = (By.XPATH, f"//nav//span[text()='{menu_name}']")
self.click(locator)
def is_menu_item_active(self, menu_name):
locator = (By.XPATH, f"//nav//span[text()='{menu_name}']/ancestor::li[contains(@class, 'active')]")
return self.is_displayed(locator)
class DashboardPage(BasePage):
"""仪表盘页面"""
def __init__(self, driver):
super().__init__(driver)
self.nav = NavigationBar(driver)
self.sidebar = Sidebar(driver)
# 通过组件操作
def navigate_to_settings(self):
self.sidebar.click_menu_item("设置")
def search_user(self, keyword):
self.nav.search(keyword)
3.2 页面工厂模式
class PageFactory:
"""页面工厂:管理页面实例"""
def __init__(self, driver):
self.driver = driver
self._pages = {}
def get_page(self, page_class):
"""获取或创建页面实例"""
if page_class not in self._pages:
self._pages[page_class] = page_class(self.driver)
return self._pages[page_class]
def create(self, page_class):
"""强制创建新的页面实例"""
self._pages[page_class] = page_class(self.driver)
return self._pages[page_class]
# 使用
factory = PageFactory(driver)
login_page = factory.get_page(LoginPage)
home_page = factory.get_page(HomePage)
3.3 链式调用设计
class LoginPage(BasePage):
def open(self):
self.driver.get(self.URL)
return self
def enter_username(self, username):
self.input_text(self.USERNAME_INPUT, username)
return self
def enter_password(self, password):
self.input_text(self.PASSWORD_INPUT, password)
return self
def submit(self):
self.click(self.LOGIN_BUTTON)
return HomePage(self.driver)
# 链式调用
HomePage(driver).open() \
.enter_username("admin") \
.enter_password("123456") \
.submit()
四、POM 项目结构
tests/
├── conftest.py # pytest fixtures
├── pages/ # Page Object 目录
│ ├── __init__.py
│ ├── base_page.py # BasePage 基类
│ ├── login_page.py # 登录页面
│ ├── home_page.py # 主页
│ ├── search_page.py # 搜索页面
│ └── components/ # 页面组件
│ ├── __init__.py
│ ├── navigation.py # 导航栏
│ └── sidebar.py # 侧边栏
├── tests/ # 测试用例目录
│ ├── test_login.py
│ ├── test_search.py
│ └── test_dashboard.py
├── data/ # 测试数据
│ ├── test_users.json
│ └── search_keywords.csv
├── utils/ # 工具类
│ ├── wait_helper.py
│ └── debug_helper.py
└── requirements.txt
五、POM 最佳实践
| 原则 | 说明 |
|---|---|
| 一个页面一个类 | 每个页面/功能模块对应一个 Page 类 |
| 定位器集中管理 | 所有定位器定义为类常量 |
| 方法返回 Page 对象 | 支持链式调用和页面流转 |
| 不暴露底层细节 | 测试代码不直接使用 find_element |
| 基类封装通用操作 | click、input_text、wait 等放在基类 |
| 组件化拆分 | 通用组件(导航栏、侧边栏)独立封装 |
六、总结
POM 的核心价值在于分离关注点:
- Page 类负责页面元素的定位和操作
- 测试类负责业务逻辑的验证
- 页面变更只修改 Page 类,不影响测试代码
这是构建可维护自动化测试框架的基础模式。
下一篇:数据驱动测试——使用外部数据源驱动测试,结合 POM 实现高效的数据管理。

浙公网安备 33010602011771号