目录

UI 自动化测试的 POM 设计模式实践

UI 自动化测试的 POM 设计模式实践

一、背景

在 UI 自动化测试中,持续维护测试脚本的成本很高。页面稍微一改动,大量的测试用例就有可能失败,需要人工修改适配。

因此,有人提出了POM(Page Object Model)设计模式。核心思维是将页面结构和测试逻辑分离,使测试代码更易于维护和扩展。

二、什么是 POM

POM 的核心思想很简单:每个页面对应一个对象,页面上的元素和操作都封装在这个对象里。测试代码不直接操作 UI 元素,而是通过 Page Object 提供的方法来执行操作。

flowchart TD subgraph Test[测试层] A[测试用例 1] B[测试用例 2] C[测试用例 3] end subgraph Page[Page Object 层] D[LoginPage] E[HomePage] F[DashboardPage] end subgraph Element[元素层] G[定位器] H[操作方法] end A & B & C --> D & E & F D & E & F --> G & H

三、为什么需要 POM

1. 没有 POM 时的问题

def test_login():
    driver.find_element(By.ID, "username").send_keys("test")
    driver.find_element(By.ID, "password").send_keys("123456")
    driver.find_element(By.CSS_SELECTOR, ".btn-primary").click()
    assert "欢迎" in driver.page_source

这种写法的问题:

  1. 元素定位散落各处:页面元素变化时需要修改大量代码来适配
  2. 业务语义不清晰:看懂代码需要耗费大量心智
  3. 代码重复严重:大量测试用例存在代码重复,没有复用

从而导致维护成本极高,开发稍微一改动,大量测试代码要跟着改。

2. 使用 POM 后

def test_login():
    login_page = LoginPage(driver)
    login_page.enter_username("test")
    login_page.enter_password("123456")
    login_page.click_login()
    assert login_page.is_logged_in()
  1. 元素定位收束:页面元素变化时只要修改一两次代码
  2. 业务语义清晰:一下看懂代码在做什么
  3. 模块化:减少重复代码

四、POM 设计实践

1. Page Object

class LoginPage:
    """登录页面对象"""

    # 元素定位
    USERNAME_INPUT = (By.ID, "username")
    PASSWORD_INPUT = (By.ID, "password")
    LOGIN_BUTTON = (By.CSS_SELECTOR, ".btn-primary")
    ERROR_MESSAGE = (By.CLASS_NAME, "error-msg")

    def __init__(self, driver):
        self.driver = driver
        self.driver.get("/login")

    # 操作方法
    def enter_username(self, username):
        self.driver.find_element(*self.USERNAME_INPUT).send_keys(username)
        return self  # 链式调用

    def enter_password(self, password):
        self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)
        return self

    def click_login(self):
        self.driver.find_element(*self.LOGIN_BUTTON).click()
        return HomePage(self.driver)  # 将进入新的页面,于是此处返回下一个页面对象

    def get_error_message(self):
        return self.driver.find_element(*self.ERROR_MESSAGE).text

    def is_logged_in(self):
        return "/home" in self.driver.current_url

2. 链式调用

def test_login_and_navigate():
    dashboard = (LoginPage(driver)
        .enter_username("admin")
        .enter_password("123456")
        .click_login() # 这里返回的是HomePage了
        .navigate_to_dashboard()) # 这里是HomePage里的方法

    assert dashboard.is_visible()

3. 组件化设计

对于过于复杂的页面,还可以将页面拆分成多个组件:

class Header:

    def __init__(self, driver):
        self.driver = driver

    def click_profile(self):
        self.driver.find_element(By.ID, "header-profile").click()
        return ProfilePage(self.driver)

    def click_logout(self):
        self.driver.find_element(By.ID, "header-logout").click()
        return LoginPage(self.driver)


class Navigation:

    def __init__(self, driver):
        self.driver = driver

    def goto_dashboard(self):
        self.driver.find_element(By.LINK_TEXT, "Dashboard").click()
        return DashboardPage(self.driver)


class DashboardPage:

    def __init__(self, driver):
        self.driver = driver
        self.header = Header(driver)
        self.navigation = Navigation(driver)

4. 封装通用能力

class BasePage:
    """页面基类,封装通用能力"""

    def __init__(self, driver, timeout=10):
        self.driver = driver
        self.timeout = timeout

    def find_element(self, locator):
        """显式等待元素出现"""
        return WebDriverWait(self.driver, self.timeout).until(
            EC.presence_of_element_located(locator)
        )

    def find_elements(self, locator):
        return WebDriverWait(self.driver, self.timeout).until(
            EC.presence_of_all_elements_located(locator)
        )

    def click(self, locator):
        """可点击等待"""
        element = self.find_element(locator)
        element.click()

    def is_element_visible(self, locator):
        try:
            return WebDriverWait(self.driver, 3).until(
                EC.visibility_of_element_located(locator)
            )
        except TimeoutException:
            return False

五、进一步扩展

1. 封装元素

对于特别复杂的页面,可以直接在元素层面封装成独立对象:

class SearchBox:

    def __init__(self, driver, locator):
        self.driver = driver
        self.locator = locator

    @property
    def input_field(self):
        return self.driver.find_element(*self.locator, By.CSS_SELECTOR, "input")

    def search(self, keyword):
        self.input_field.clear()
        self.input_field.send_keys(keyword)
        self.input_field.send_keys(Keys.ENTER)
        return SearchResultsPage(self.driver)


class HomePage:
    def __init__(self, driver):
        self.driver = driver
        self.search_box = SearchBox(driver, (By.ID, "global-search"))

2. 数据驱动

@dataclass
class LoginInfo:
    username: str
    password: str
    expected_success: bool


class LoginPage:
    def login(self, info: LoginInfo):
        self.enter_username(info.username)
        self.enter_password(info.password)
        page = self.click_login()

        if info.expected_success:
            assert isinstance(page, HomePage)
        else:
            assert self.get_error_message() is not None

        return page

六、总结

好的 POM 设计能让测试代码像业务文档一样可读,同时极大降低维护成本。刚开始可能会觉得"多此一举",但当页面迭代、用例增长到一定规模时,POM 的价值会自然体现出来。