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
基类封装通用操作 clickinput_textwait 等放在基类
组件化拆分 通用组件(导航栏、侧边栏)独立封装

六、总结

POM 的核心价值在于分离关注点

  • Page 类负责页面元素的定位和操作
  • 测试类负责业务逻辑的验证
  • 页面变更只修改 Page 类,不影响测试代码

这是构建可维护自动化测试框架的基础模式。

下一篇:数据驱动测试——使用外部数据源驱动测试,结合 POM 实现高效的数据管理。

posted @ 2026-04-07 16:03  小小阿狸。  阅读(0)  评论(0)    收藏  举报