Selenium 页面对象模式 #
什么是页面对象模式 #
概念介绍 #
text
┌─────────────────────────────────────────────────────────────┐
│ 页面对象模式 (Page Object Model) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 核心思想: │
│ 将页面元素定位和操作封装到独立的类中, │
│ 测试用例只关注业务逻辑,不关心具体实现细节。 │
│ │
│ 优势: │
│ ✅ 代码复用 - 相同操作只需编写一次 │
│ ✅ 易于维护 - 元素变化只需修改一处 │
│ ✅ 清晰结构 - 测试与页面分离 │
│ ✅ 可读性强 - 测试代码更接近业务语言 │
│ │
│ 结构: │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 测试用例 │───>│ 页面对象 │ │
│ │ (test_*.py) │ │ (*_page.py) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ 基础页面 │ │
│ │ │ (base_page.py) │ │
│ │ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ WebDriver │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
传统方式 vs POM #
python
# 传统方式 - 所有代码混在一起
def test_login():
driver = webdriver.Chrome()
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys("user")
driver.find_element(By.ID, "password").send_keys("pass")
driver.find_element(By.ID, "submit").click()
assert "Dashboard" in driver.title
driver.quit()
# POM 方式 - 清晰分离
def test_login(driver, login_page):
login_page.open()
login_page.login("user", "pass")
assert login_page.is_login_success()
基础页面类 #
BasePage 实现 #
python
# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
class BasePage:
"""基础页面类,封装通用操作"""
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def open(self, url):
"""打开页面"""
self.driver.get(url)
def find_element(self, locator, timeout=10):
"""查找元素"""
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.presence_of_element_located(locator))
def find_elements(self, locator, timeout=10):
"""查找多个元素"""
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.presence_of_all_elements_located(locator))
def click(self, locator, timeout=10):
"""点击元素"""
element = WebDriverWait(self.driver, timeout).until(
EC.element_to_be_clickable(locator)
)
element.click()
def enter_text(self, locator, text, timeout=10):
"""输入文本"""
element = self.find_element(locator, timeout)
element.clear()
element.send_keys(text)
def get_text(self, locator, timeout=10):
"""获取元素文本"""
element = self.find_element(locator, timeout)
return element.text
def get_attribute(self, locator, attribute, timeout=10):
"""获取元素属性"""
element = self.find_element(locator, timeout)
return element.get_attribute(attribute)
def is_visible(self, locator, timeout=10):
"""元素是否可见"""
try:
WebDriverWait(self.driver, timeout).until(
EC.visibility_of_element_located(locator)
)
return True
except:
return False
def is_element_present(self, locator, timeout=10):
"""元素是否存在"""
try:
self.find_element(locator, timeout)
return True
except:
return False
def wait_for_invisible(self, locator, timeout=10):
"""等待元素不可见"""
WebDriverWait(self.driver, timeout).until(
EC.invisibility_of_element_located(locator)
)
def wait_for_text(self, locator, text, timeout=10):
"""等待元素包含文本"""
WebDriverWait(self.driver, timeout).until(
EC.text_to_be_present_in_element(locator, text)
)
def scroll_to_element(self, locator):
"""滚动到元素"""
element = self.find_element(locator)
self.driver.execute_script(
"arguments[0].scrollIntoView({block: 'center'});", element
)
def hover(self, locator):
"""悬停到元素"""
element = self.find_element(locator)
ActionChains(self.driver).move_to_element(element).perform()
def take_screenshot(self, filename):
"""截图"""
self.driver.save_screenshot(filename)
def get_title(self):
"""获取页面标题"""
return self.driver.title
def get_url(self):
"""获取当前 URL"""
return self.driver.current_url
页面对象实现 #
登录页面 #
python
# pages/login_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
class LoginPage(BasePage):
"""登录页面对象"""
# 元素定位器
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.ID, "submit")
ERROR_MESSAGE = (By.CLASS_NAME, "error-message")
FORGOT_PASSWORD_LINK = (By.LINK_TEXT, "忘记密码")
REGISTER_LINK = (By.LINK_TEXT, "注册")
# 页面 URL
URL = "/login"
def __init__(self, driver, base_url):
super().__init__(driver)
self.base_url = base_url
def open(self):
"""打开登录页面"""
super().open(f"{self.base_url}{self.URL}")
def enter_username(self, username):
"""输入用户名"""
self.enter_text(self.USERNAME_INPUT, username)
def enter_password(self, password):
"""输入密码"""
self.enter_text(self.PASSWORD_INPUT, password)
def click_login(self):
"""点击登录按钮"""
self.click(self.LOGIN_BUTTON)
def login(self, username, password):
"""执行登录操作"""
self.enter_username(username)
self.enter_password(password)
self.click_login()
def get_error_message(self):
"""获取错误消息"""
return self.get_text(self.ERROR_MESSAGE)
def is_error_displayed(self):
"""错误消息是否显示"""
return self.is_visible(self.ERROR_MESSAGE)
def click_forgot_password(self):
"""点击忘记密码"""
self.click(self.FORGOT_PASSWORD_LINK)
def click_register(self):
"""点击注册链接"""
self.click(self.REGISTER_LINK)
def is_login_page(self):
"""是否在登录页面"""
return "/login" in self.get_url()
首页 #
python
# pages/home_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
class HomePage(BasePage):
"""首页对象"""
# 元素定位器
WELCOME_MESSAGE = (By.ID, "welcome")
USER_MENU = (By.ID, "user-menu")
LOGOUT_BUTTON = (By.ID, "logout")
SEARCH_INPUT = (By.ID, "search")
SEARCH_BUTTON = (By.ID, "search-btn")
NAV_MENU = (By.CSS_SELECTOR, ".nav-menu")
def __init__(self, driver, base_url):
super().__init__(driver)
self.base_url = base_url
def open(self):
"""打开首页"""
super().open(self.base_url)
def get_welcome_message(self):
"""获取欢迎消息"""
return self.get_text(self.WELCOME_MESSAGE)
def is_logged_in(self):
"""是否已登录"""
return self.is_visible(self.USER_MENU)
def logout(self):
"""退出登录"""
self.hover(self.USER_MENU)
self.click(self.LOGOUT_BUTTON)
def search(self, keyword):
"""执行搜索"""
self.enter_text(self.SEARCH_INPUT, keyword)
self.click(self.SEARCH_BUTTON)
def navigate_to_menu(self, menu_name):
"""导航到菜单"""
menu_locator = (By.LINK_TEXT, menu_name)
self.click(menu_locator)
搜索结果页 #
python
# pages/search_results_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage
class SearchResultsPage(BasePage):
"""搜索结果页对象"""
# 元素定位器
RESULTS_CONTAINER = (By.ID, "results")
RESULT_ITEMS = (By.CSS_SELECTOR, ".result-item")
NO_RESULTS_MESSAGE = (By.CLASS_NAME, "no-results")
PAGINATION = (By.CSS_SELECTOR, ".pagination")
def get_result_count(self):
"""获取结果数量"""
items = self.find_elements(self.RESULT_ITEMS)
return len(items)
def get_result_titles(self):
"""获取所有结果标题"""
items = self.find_elements(self.RESULT_ITEMS)
return [item.find_element(By.TAG_NAME, "h3").text for item in items]
def click_result(self, index):
"""点击指定结果"""
items = self.find_elements(self.RESULT_ITEMS)
if 0 <= index < len(items):
items[index].click()
def has_results(self):
"""是否有结果"""
return self.is_visible(self.RESULTS_CONTAINER) and self.get_result_count() > 0
def has_no_results(self):
"""是否无结果"""
return self.is_visible(self.NO_RESULTS_MESSAGE)
def go_to_page(self, page_number):
"""跳转到指定页"""
page_link = (By.CSS_SELECTOR, f".pagination a[data-page='{page_number}']")
self.click(page_link)
测试用例实现 #
使用页面对象的测试 #
python
# tests/test_login.py
import pytest
from pages.login_page import LoginPage
from pages.home_page import HomePage
class TestLogin:
"""登录测试"""
@pytest.fixture(autouse=True)
def setup(self, driver, base_url):
self.login_page = LoginPage(driver, base_url)
self.home_page = HomePage(driver, base_url)
def test_login_success(self):
"""测试登录成功"""
self.login_page.open()
self.login_page.login("user", "password")
assert self.home_page.is_logged_in()
assert "Welcome" in self.home_page.get_welcome_message()
def test_login_invalid_password(self):
"""测试密码错误"""
self.login_page.open()
self.login_page.login("user", "wrong_password")
assert self.login_page.is_error_displayed()
assert "密码错误" in self.login_page.get_error_message()
def test_login_empty_username(self):
"""测试用户名为空"""
self.login_page.open()
self.login_page.login("", "password")
assert self.login_page.is_error_displayed()
def test_navigate_to_register(self):
"""测试跳转注册页面"""
self.login_page.open()
self.login_page.click_register()
assert "/register" in self.login_page.get_url()
搜索测试 #
python
# tests/test_search.py
import pytest
from pages.home_page import HomePage
from pages.search_results_page import SearchResultsPage
class TestSearch:
"""搜索测试"""
@pytest.fixture(autouse=True)
def setup(self, driver, base_url):
self.home_page = HomePage(driver, base_url)
self.search_results = SearchResultsPage(driver, base_url)
def test_search_with_results(self):
"""测试有结果的搜索"""
self.home_page.open()
self.home_page.search("Selenium")
assert self.search_results.has_results()
assert self.search_results.get_result_count() > 0
def test_search_no_results(self):
"""测试无结果的搜索"""
self.home_page.open()
self.home_page.search("不存在的关键词xyz123")
assert self.search_results.has_no_results()
@pytest.mark.parametrize("keyword,expected_min", [
("Python", 5),
("Java", 3),
("JavaScript", 4),
])
def test_search_results_count(self, keyword, expected_min):
"""测试搜索结果数量"""
self.home_page.open()
self.home_page.search(keyword)
assert self.search_results.get_result_count() >= expected_min
高级模式 #
组件对象 #
python
# pages/components/header_component.py
from selenium.webdriver.common.by import By
class HeaderComponent:
"""页面头部组件"""
# 元素定位器
LOGO = (By.ID, "logo")
SEARCH_INPUT = (By.ID, "header-search")
USER_MENU = (By.ID, "user-menu")
CART_ICON = (By.ID, "cart-icon")
def __init__(self, driver):
self.driver = driver
def search(self, keyword):
"""搜索"""
self.driver.find_element(*self.SEARCH_INPUT).send_keys(keyword)
self.driver.find_element(*self.SEARCH_INPUT).submit()
def go_to_home(self):
"""返回首页"""
self.driver.find_element(*self.LOGO).click()
def get_cart_count(self):
"""获取购物车数量"""
cart = self.driver.find_element(*self.CART_ICON)
return cart.find_element(By.CLASS_NAME, "count").text
def is_user_logged_in(self):
"""用户是否登录"""
try:
self.driver.find_element(*self.USER_MENU)
return True
except:
return False
# pages/product_page.py
from pages.base_page import BasePage
from pages.components.header_component import HeaderComponent
class ProductPage(BasePage):
"""商品页面"""
def __init__(self, driver, base_url):
super().__init__(driver)
self.base_url = base_url
self.header = HeaderComponent(driver) # 组合组件
def search_from_header(self, keyword):
"""通过头部组件搜索"""
self.header.search(keyword)
页面对象工厂 #
python
# pages/page_factory.py
from pages.login_page import LoginPage
from pages.home_page import HomePage
from pages.search_results_page import SearchResultsPage
class PageFactory:
"""页面对象工厂"""
PAGES = {
'login': LoginPage,
'home': HomePage,
'search_results': SearchResultsPage,
}
@staticmethod
def create(page_name, driver, base_url):
"""创建页面对象"""
page_class = PageFactory.PAGES.get(page_name)
if page_class:
return page_class(driver, base_url)
raise ValueError(f"未知的页面: {page_name}")
# 使用示例
def test_example(driver, base_url):
login_page = PageFactory.create('login', driver, base_url)
login_page.open()
链式调用 #
python
# pages/base_page.py
class BasePage:
"""支持链式调用的基础页面"""
def open(self, url):
"""打开页面"""
self.driver.get(url)
return self
def click(self, locator):
"""点击元素"""
self.find_element(locator).click()
return self
def enter_text(self, locator, text):
"""输入文本"""
element = self.find_element(locator)
element.clear()
element.send_keys(text)
return self
# pages/login_page.py
class LoginPage(BasePage):
"""支持链式调用的登录页面"""
def enter_username(self, username):
self.enter_text(self.USERNAME_INPUT, username)
return self
def enter_password(self, password):
self.enter_text(self.PASSWORD_INPUT, password)
return self
def click_login(self):
self.click(self.LOGIN_BUTTON)
return self
def login(self, username, password):
"""链式登录"""
return (self.enter_username(username)
.enter_password(password)
.click_login())
# 使用示例
def test_login_chain(driver, base_url):
login_page = LoginPage(driver, base_url)
login_page.open().enter_username("user").enter_password("pass").click_login()
最佳实践 #
元素定位器管理 #
python
# pages/locators.py
from selenium.webdriver.common.by import By
class LoginPageLocators:
"""登录页面定位器"""
USERNAME = (By.ID, "username")
PASSWORD = (By.ID, "password")
SUBMIT = (By.ID, "submit")
ERROR = (By.CLASS_NAME, "error")
class HomePageLocators:
"""首页定位器"""
WELCOME = (By.ID, "welcome")
USER_MENU = (By.ID, "user-menu")
LOGOUT = (By.ID, "logout")
# pages/login_page.py
from pages.locators import LoginPageLocators as Locators
class LoginPage(BasePage):
USERNAME_INPUT = Locators.USERNAME
PASSWORD_INPUT = Locators.PASSWORD
# ...
动态定位器 #
python
# pages/base_page.py
class BasePage:
def get_dynamic_locator(self, base_locator, value):
"""生成动态定位器"""
by, locator = base_locator
return (by, locator.format(value))
# 使用示例
class ProductPage(BasePage):
PRODUCT_ITEM = (By.CSS_SELECTOR, ".product-item[data-id='{}']")
def get_product_by_id(self, product_id):
locator = self.get_dynamic_locator(self.PRODUCT_ITEM, product_id)
return self.find_element(locator)
页面状态验证 #
python
# pages/login_page.py
class LoginPage(BasePage):
def is_page_loaded(self):
"""验证页面是否加载完成"""
return (self.is_element_present(self.USERNAME_INPUT) and
self.is_element_present(self.PASSWORD_INPUT) and
self.is_element_present(self.LOGIN_BUTTON))
def wait_for_page_load(self, timeout=10):
"""等待页面加载"""
WebDriverWait(self.driver, timeout).until(
lambda d: self.is_page_loaded()
)
完整示例 #
项目结构 #
text
project/
├── config/
│ └── settings.py
├── pages/
│ ├── __init__.py
│ ├── base_page.py
│ ├── login_page.py
│ ├── home_page.py
│ └── components/
│ └── header.py
├── tests/
│ ├── conftest.py
│ ├── test_login.py
│ └── test_search.py
└── pytest.ini
python
# tests/conftest.py
import pytest
from selenium import webdriver
from pages.login_page import LoginPage
from pages.home_page import HomePage
from config.settings import config
@pytest.fixture
def driver():
driver = webdriver.Chrome()
driver.maximize_window()
yield driver
driver.quit()
@pytest.fixture
def base_url():
return config.base_url
@pytest.fixture
def login_page(driver, base_url):
return LoginPage(driver, base_url)
@pytest.fixture
def home_page(driver, base_url):
return HomePage(driver, base_url)
# tests/test_login.py
class TestLogin:
def test_login_success(self, login_page, home_page):
login_page.open()
login_page.login(config.username, config.password)
assert home_page.is_logged_in()
下一步 #
掌握了页面对象模式后,接下来学习 最佳实践 了解更多测试优化技巧!
最后更新:2026-03-28