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