在自动化测试框架中,我们经常使用POM(Page Object Model)模式对被测试页面进行封装抽象。

然而,在实际开发过程中,我们常常看到由于对 POM 理解不深或实践方式不当,导致测试代码结构逐渐偏离其初衷,使得自动化测试框架反而变得臃肿、难以维护。

本文将梳理POM的常见反模式(Anti-Pattern),帮助你在项目中更好地实践和落地POM模式。

什么是页面对象模型(POM)

POM本质上是一种设计模式,用于创建网页元素的对象库。其核心目的是建立机制,为框架提供更健壮、稳定和灵活的设计。

在页面对象类的开发时,我们为不同的页面创建一个对应的页面类,每个类中封装该页面上的元素定位器以及常用操作方法。

这种方式实现了页面元素与测试逻辑的解耦,多个测试脚本可以复用同一页面类中的元素与方法,从而大大减少了代码重复,提升了整体的可维护性与扩展性。

为什么使用POM

01 可维护性

在 Web 应用的开发过程中,如果测试代码直接去操作页面上的对象,一旦 UI 发生变动测试用例就可能全部失效,就会造成散弹式的修改,维护成本很高。

假设有多个不同的登录测试用例(如不同用户名、密码错误、验证码验证等)。一旦页面中的某个元素发生改变,或者登录页面多了一个验证框时、所有涉及登录的测试用例都需要修改。

POM对被测试页面进行抽象和封装,将页面元素和操作方法交互逻辑集中在一个类中。当 UI 发生变化时,只需要修改对应的Page类,从而提升测试用例代码的可维护性。

02 代码复用性

POM通过封装页面上的操作可以使不同的测试脚本可以复用相同的交互逻辑,减少代码重复。

例如,多个测试用例可能都需要登录功能,如果每个测试都手动输入用户名和密码,代码会大量冗余。但使用 POM,可以封装 login() 方法,在不同的测试中直接调用,提高代码复用性。

03 让测试逻辑与UI 结构解耦

使用了POM设计后,就间接的为测试框架引入了分层设计的概念,上层的测试逻辑和底层的页面解耦。

这种解耦方式使得测试层的代码可以专注于测试本身,而不依赖具体的操作或元素。

POM的设计原则

单一职责原则(SRP):

每个页面对象类应只负责一个页面或页面的一小部分功能。这样可以保持代码结构清晰、有条理,便于维护,同时明确不同功能区域的职责边界。

抽象性(Abstraction):

页面对象应屏蔽页面交互的细节,例如元素定位和具体操作方法。通过提供更直观、可读性强的接口,抽象能够降低测试代码的脆弱性,并提升其可维护性。

封装性(Encapsulation):

页面对象应封装页面的状态与行为,使开发者可以轻松理解当前页面的状态以及可执行的操作。这有助于保持关注点分离,并提高测试代码的可读性。

可复用性(Reusability):

页面对象应被设计为可在多个测试中复用,从而减少代码重复,提高测试开发的效率。通过构建模块化、自包含的页面对象,可以像积木一样快速组合测试逻辑。

易读性(Easy to Understand): 

页面对象中的方法和变量命名应具备良好的可读性和表达力,让开发者无需阅读底层代码即可理解其作用。清晰的命名有助于提升测试代码的可读性与可维护性。

关注点分离(Separation of Concerns):

测试脚本应专注于描述页面的高层行为,而页面对象则负责底层的页面操作细节。实现这种职责分离可以让测试代码更清晰、更易组织,同时构建出更具可扩展性的测试框架。

POM中的常见的Anti Pattern

01 在 Page Object 中添加断言或验证逻辑

最最常见的一个习惯,估计就是在POM中加入断言了,比如登录方法中直接写上判断登录是否成功。因为这样似乎就能复用了,但是等一下!

在页面对象方法中包含断言模糊了关注点。页面对象的职责应仅限于封装页面上的元素操作,而断言属于测试验证逻辑,应该由测试用例负责。将断言写在页面类中,会导致职责混乱,降低测试用例的灵活性与可维护性。

❌ 不推荐的做法:在页面对象中断言


  1. # page_objects.py

  2. class LoginPage:

  3.     def __init__(self, driver):

  4.         self.driver = driver

  5.         self.username_input = driver.find_element_by_id("username")

  6.         self.password_input = driver.find_element_by_id("password")

  7.         self.login_button = driver.find_element_by_id("login_button")

  8.         self.welcome_message = driver.find_element_by_id("welcome_message")

  9.  

  10.     def login(self, username, password):

  11.         self.username_input.send_keys(username)

  12.         self.password_input.send_keys(password)

  13.         self.login_button.click()

  14.         assert "Welcome" in self.welcome_message.text, "Login failed"

 

✅ 推荐的做法:将断言放在测试用例中​​​​​​​


  1. # page_objects.py

  2. class LoginPage:

  3.     def __init__(self, driver):

  4.         self.driver = driver

  5.         self.username_input = driver.find_element_by_id("username")

  6.         self.password_input = driver.find_element_by_id("password")

  7.         self.login_button = driver.find_element_by_id("login_button")

  8.     def login(self, username, password):

  9.         self.username_input.send_keys(username)

  10.         self.password_input.send_keys(password)

  11.         self.login_button.click()

  12.     def get_welcome_message(self):

  13.         return self.driver.find_element_by_id("welcome_message").text

  14. # test_login.py

  15. def test_login_success():

  16.     login_page = LoginPage(driver)

  17.     login_page.login("user123", "password123")

  18.     welcome_message = login_page.get_welcome_message()

  19.     assert "Welcome" in welcome_message, "Login failed"

 

将断言逻辑混入页面对象POM中主要带来的问题:

1. 限制用例扩展,影响多场景测试

就拿登录举例,当需要测试多个登录场景(成功、密码错误、用户名为空等),但断言已经写死在 login() 方法中,导致你不得不复制代码或创建多个方法

(如 login_with_valid_credentials()、login_with_invalid_password()),这反而增加了代码重复和维护成本。

2. 造成不必要的失败

假设你有一个测试流程是:创建用户 → 删除用户。测试的重点是验证删除功能是否正常,但如果在“创建用户”这一步就加了断言(验证欢迎信息等),一旦这个验证因为意外失败(比如延迟),就会阻断后续步骤,让真正想测试的删除功能无从验证。​​​​​​​


  1. def test_create_and_delete_user():

  2.     login_page.login("admin", "admin123")  

  3.     user_page.create_user("new_user")  # 创建时断言意外失败

  4.     user_page.delete_user("new_user")  # 没有执行到这里

3. 引入复杂的验证逻辑或外部依赖

有时验证结果需要调用接口、查数据库、检查后端日志等,这些属于测试逻辑范畴,不应混入页面类中。如果你把这类逻辑塞入 Page 类,不但代码臃肿,还会造成页面类对测试环境有依赖, 违背了测试框架的分层设计。​​​​​​​

  1. def login(self, username, password):

  2.     # ❌ 页面对象中引入数据库验证

  3.     db_user = query_user_from_db(username)

  4.     assert db_user.active, "User not activated"

02 在测试函数中直接操作元素

在 Page Object 模式中,一个关键的最佳实践是:不要在测试脚本中直接使用 locator 来操作页面元素。相反,应当在页面对象类中封装好所有的元素定位与相关操作逻辑。

❌ 不推荐示例:没有封装元素或操作逻辑​​​​​​​

  1. # Playwright 示例

  2. class LoginPage:

  3.     def __init__(self, page):

  4.         self.page = page

  5.         self.email_field = page.locator("id=input-email")

  6.         self.password_field = page.locator("id=input-password")

  7.         self.login_button = page.locator('input:has-text("Login")')

  8.         self.welcome_message = page.locator("#welcome")

  9. # test_login.py

  10. def test_login_success(page):

  11.     login_page = LoginPage(page)

  12.     login_page.email_field.fill("user@example.com")     # ❌ 操作在测试中进行

  13.     login_page.password_field.fill("password123")       # ❌

  14.     login_page.login_button.click()                     # ❌

  15.     assert login_page.welcome_message.is_visible()      #

 

这种方式会破坏POM对象的封装性和抽象性,背离了设计初衷。我们会将所有的操作细节暴露或转移到了测试层中,会让测试用例不仅要描述“测试逻辑”,还必须处理“操作细节”,从而使测试代码变得冗长、难以阅读和维护。

想象一下,当一个测试用例需要执行多个步骤、操作多个页面元素时,这种将所有操作展开在测试层的方式会让测试脚本变得非常臃肿。

✅ 推荐做法:封装元素定位与操作逻辑

✔️ Selenium 示例​​​​​​​


  1. # login_page.py

  2. from selenium.webdriver.common.by import By

  3. from selenium.webdriver.remote.webdriver import WebDriver

  4. class LoginPage:

  5.     def __init__(self, driver: WebDriver):

  6.         self.driver = driver

  7.         self._username_locator = (By.ID, "username")

  8.         self._password_locator = (By.ID, "password")

  9.         self._login_button_locator = (By.ID, "login")

  10.     def login(self, username, password):

  11.         self.driver.find_element(self._username_locator).send_keys(username)        self.driver.find_element(self._password_locator).send_keys(password)

  12.         self.driver.find_element(*self._login_button_locator).click()

  13.     def is_logged_in(self) -> bool:

  14.         return "Welcome" in self.driver.page_source

  15. # test_login.py

  16. def test_login_success(driver):

  17.     login_page = LoginPage(driver)

  18.     login_page.login("user@example.com", "password123")

  19.     assert login_page.is_logged_in()

✔️ Playwright 示例​​​​​​​

  1. # login_page.py

  2. from playwright.sync_api import Page

  3. class LoginPage:

  4.     def __init__(self, page: Page):

  5.         self.page = page

  6.         self._email_field = page.locator("id=input-email")

  7.         self._password_field = page.locator("id=input-password")

  8.         self._login_button = page.locator('input:has-text("Login")')

  9.         self._welcome_message = page.locator("#welcome")

  10.     def enter_email(self, email: str):

  11.         self._email_field.fill(email)

  12.     def enter_password(self, password: str):

  13.         self._password_field.fill(password)

  14.  

  15.     def click_login(self):

  16.         self._login_button.click()

  17.     def login(self, email: str, password: str):

  18.         self.enter_email(email)

  19.         self.enter_password(password)

  20.         self.click_login()

  21.     def is_logged_in(self) -> bool:

  22.         return self._welcome_message.is_visible()

  23. # test_login.py

  24. def test_login_success(page):

  25.     login_page = LoginPage(page)

  26.     login_page.login("user@example.com", "password123")

  27.     assert login_page.is_logged_in()

这边说明一下,在Python中虽然没有通过关键字强制私有属性的设计。可以通过在属性前加下划线(如 _email_field)表示该属性是私有内部使用的,调用者应该遵守约定不应该类外直接访问

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐