找回密码
 立即注册
首页 业界区 安全 笔记:Selenium 的 PO 模式

笔记:Selenium 的 PO 模式

蓟晓彤 2025-7-13 15:40:06
本文记录了我对 Selenium 的 PO 模式一些理解,其中包含 PO 模式是什么,以及相应的示例。
在我第一眼看到 PO 模式(Page Object Model)时,心里就有两个疑惑:

  • 这是什么东东?
  • 我又该如何使用?
带着这两个疑惑,去查找资料了解到,它是一种设计模式,将定位和操作页面元素的代码与测试代码相分离,以便提高测试的可维护性和复用性。
也就是说,对页面元素的定位和操作进行封装,然后提供一个向上的统一接口,再进行调用。
这么做有一个好处,如果页面元素发生变化(如标签 id 改变,html 结构变化等),只需要对先前封装的代码进行调整,而无需改变测试代码。
这里有个一例子,是没有使用 PO 模式的代码:
  1. import pytest
  2. from selenium import webdriver
  3. from selenium.webdriver.common.by import By
  4. from selenium.webdriver.support.ui import WebDriverWait
  5. from selenium.webdriver.support import expected_conditions as EC
  6. @pytest.fixture  
  7. def driver():  
  8.     driver = webdriver.Edge()  
  9.     driver.get("https://example.com/login")  
  10.     yield driver  
  11.     driver.quit()
  12. def test_login_without_po(driver):
  13.         # 直接在测试用例中定位和操作元素
  14.         # 等待用户名输入框加载完成
  15.         username_input = WebDriverWait(driver, 10).until(
  16.                 EC.presence_of_element_located((By.ID, "username"))
  17.         )
  18.         username_input.send_keys("test_user")
  19.        
  20.         # 等待密码输入框加载完成
  21.         password_input = WebDriverWait(driver, 10).until(
  22.                 EC.presence_of_element_located((By.ID, "password"))
  23.         )
  24.         password_input.send_keys("test_password")
  25.        
  26.         # 等待登录按钮加载完成并点击
  27.         login_button = WebDriverWait(driver, 10).until(
  28.                 EC.element_to_be_clickable((By.ID, "login-button"))
  29.         )
  30.         login_button.click()
  31.        
  32.         # 验证登录成功后的欢迎消息
  33.         login_message = WebDriverWait(driver, 10).until(
  34.                 EC.visibility_of_element_located((By.ID, "login_message"))
  35.         )
  36.         assert "Welcome" in login_message.text
复制代码
可以看到大量的元素操作细节和测试逻辑混合在一起,不仅违背了单一职责原则,还导致大量重复代码出现,而它们之间的区别仅仅是页面元素不同。
试想一下,目前只是针对登录功能的测试,所以代码量看起来比较小,但如果要测试整个网站的功能(如注册、注销、修改个人资料等),那同样的页面元素的定位和操作岂不是每个测试中都要再重复出现一次。
并且,在开发过程中网页结构是会不断变化的,每一次变化,我们都需要重新调整测试代码,一两个还好,但架不住量大啊!
我自诩一位懒人,自然要尽可能的偷懒喽。
OP 模式的核心概念

写到这里,必然要介绍一番 OP 模式的核心概念,以便在下面的示例中有更好的理解。

  • Page object(页面对象)

    • 将每个网页抽象为一个类,页面中的元素和操作封装为类的属性和方法。
    • 例如,登录页面可抽象为 LoginPage 类。

  • 页面操作与测试用例的分离关注点

    • 页面对象:负责元素定位、交互操作(如点击、输入)。
    • 测试用例:专注于业务逻辑,调用页面对象的方法完成测试。

PO 模式的实现示例

现在开始实操。根据上面所述,将页面操作和测试用例进行分离,那么可以将项目划分为:

  • 页面对象层:每个页面一个类,封装元素和操作。
  • 测试用例层:调用页面对象完成测试。
项目结构如下:
  1. my_project/
  2. ├── pages/
  3. │   ├── page_login.py
  4. │   └── ...
  5. └── tests/
  6.     ├── test_login.py
  7.     └── ...
复制代码
定义 page_login.py

假设我们要测试一个登录界面,可以创建一个 LoginPage 类,封装“登录”操作。
  1. from selenium.webdriver.common.by import By
  2. from selenium.webdriver.support.ui import WebDriverWait
  3. from selenium.webdriver.support import expected_conditions as EC
  4. class LoginPage:
  5.     def __init__(self, driver):
  6.         """初始化 LoginPage 实例。
  7.         
  8.         参数:
  9.             driver (WebDriver): 传入的 Selenium WebDriver 实例,
  10.                               用于与浏览器进行交互。
  11.         """
  12.         self.__driver = driver
  13.         self.__username_input = (By.ID, "username")
  14.         self.__password_input = (By.ID, "password")
  15.         self.__login_button = (By.ID, "login_button")
  16.         self.__login_message = (By.ID, "login_message")
  17.     def login(self, username, password):
  18.         """登录系统。
  19.         参数:
  20.             username (str): 用户名
  21.             password (str): 密码
  22.         """
  23.         self.__enter_username(username)
  24.         self.__enter_password(password)
  25.         self.__click_login()
  26.     def get_login_message(self):
  27.         """获取登录消息文本。
  28.         返回:
  29.             str: 登录消息的文本内容。
  30.         """
  31.         return WebDriverWait(self.__driver, 10).until(
  32.             EC.visibility_of_element_located(self.__login_message)
  33.         ).text
  34.     def __enter_username(self, username):
  35.         """在用户名输入框中输入指定的用户名。
  36.         参数:
  37.             username (str): 要输入的用户名
  38.         """
  39.         WebDriverWait(self.__driver, 10).until(
  40.             EC.visibility_of_element_located(self.__username_input)
  41.         ).send_keys(username)
  42.     def __enter_password(self, password):
  43.         """在密码输入框中输入指定的密码。
  44.         参数:
  45.             password (str): 要输入的密码
  46.         """
  47.         WebDriverWait(self.__driver, 10).until(
  48.             EC.visibility_of_element_located(self.__password_input)
  49.         ).send_keys(password)
  50.     def __click_login(self):
  51.         """等待登录按钮可点击,并执行点击操作。"""
  52.         WebDriverWait(self.__driver, 10).until(
  53.             EC.element_to_be_clickable(self.__login_button)
  54.         ).click()
复制代码
定义 test_login.py

现在创建 test_login.py 来测试登录功能。
  1. import pytest
  2. from selenium import webdriver
  3. from pages.page_login import LoginPage
  4. @pytest.fixture
  5. def driver():
  6.     """创建并配置一个Edge WebDriver实例,用于测试登录功能。
  7.     返回:
  8.         WebDriver: 配置好的Edge WebDriver实例
  9.     """
  10.     driver = webdriver.Edge()
  11.     driver.get("https://example.com/login")
  12.     yield driver
  13.     driver.quit()
  14. def test_login(driver):
  15.     """测试登录功能。
  16.     参数:
  17.         driver (WebDriver): Selenium WebDriver实例,用于浏览器自动化操作。
  18.     """
  19.     login_page = LoginPage(driver)
  20.     login_page.login("test_user", "test_password")
  21.     assert "Welcome" in login_page.get_login_message(), "登录失败,未找到欢迎信息"
复制代码
进一步封装:base.py

到这里,这个例子完善了吗?不,关于页面元素的操作还可以进一步封装,将共有的操作提出出来。
这么说可能觉得不明所以,心里会有一个疑问:“是可以再进一步封装,但为什么要呢?”
这个答案在于,为了可维护性。试想一下,假如在一个程序中,我们将要频繁的使用 "passwd" 这个字符串,是使用一个变量保存它,还是直接使用?
当然是选择定义一个变量保存,这样如果日后有需要,就可以更改一处,得到全局自动获取新内容。
在这里,也是一样的,将相同的操作抽离出来,如果以后有更好的实现方式,就可以只更改这一处,而无需更改其他代码。
所以我们再添加一个基础层,用来存放通用操作。
更新后的项目结构如下:
  1. my_project/
  2. ├── bases/        # 新添加
  3. │   ├── base.py   # 存放通用操作
  4. │   └── ...
  5. ├── pages/
  6. │   ├── page_login.py
  7. │   └── ...
  8. └── tests/
  9.     ├── test_login.py
  10.     └── ...
复制代码
定义 base.py

定义 Base 类,存放元素查找,和一些常见的操作方法(如点击、输入文本等)。
  1. from selenium.webdriver.support.wait import WebDriverWait
  2. from selenium.webdriver.support import expected_conditions as EC
  3. class Base:
  4.     def __init__(self, driver):
  5.         """初始化Base类。
  6.         参数:
  7.             driver (WebDriver): WebDriver实例,用于与浏览器进行交互。
  8.         """
  9.         self.driver = driver
  10.     def base_find_element(self, locator, timeout=10):
  11.         """等待指定的元素在页面上变得可见。
  12.         参数:
  13.             locator (tuple): 用于定位元素的元组,通常由By和定位值组成。
  14.             timeout (int, optional): 等待元素的最大时间(秒)。默认为10秒。
  15.         返回:
  16.             WebElement: 等待到的可见元素。
  17.         """
  18.         return WebDriverWait(self.driver, timeout=timeout).until(
  19.             EC.visibility_of_element_located(locator)
  20.         )
  21.     def base_click(self, locator):
  22.         """执行元素点击操作。
  23.         参数:
  24.             locator (tuple): 定位元素的元组,包含定位方式和定位值。
  25.         """
  26.         self.base_find_element(locator).click()
  27.     def base_send_keys(self, locator, text):
  28.         """在定位到的元素中输入文本。
  29.         参数:
  30.             locator (tuple): 定位元素的元组,通常包含定位方式和定位值。
  31.             text (str): 需要输入的文本内容。
  32.         """
  33.         element = self.base_find_element(locator)
  34.         element.clear()
  35.         element.send_keys(text)
复制代码
修改 page_login.py

目前只需要修改 page_login.py,而无需动测试用例,因为我们已经为测试用例提供了统一的调用接口,只要我们不改动这个,就可以。封装的魅力。
  1. from selenium.webdriver.common.by import By
  2. from bases.base import Base
  3. class LoginPage:
  4.     def __init__(self, driver):
  5.         """初始化 LoginPage 实例。
  6.         
  7.         参数:
  8.             driver (WebDriver): 传入的 Selenium WebDriver 实例,
  9.                               用于与浏览器进行交互。
  10.         """
  11.         self.__base = Base(driver)
  12.         self.__username_input = (By.ID, "username")
  13.         self.__password_input = (By.ID, "password")
  14.         self.__login_button = (By.ID, "login_button")
  15.         self.__login_message = (By.ID, "login_message")
  16.     def login(self, username, password):
  17.         """登录系统
  18.         参数:
  19.             username (str): 用户名
  20.             password (str): 密码
  21.         """
  22.         self.__enter_username(username)
  23.         self.__enter_password(password)
  24.         self.__click_login()
  25.     def get_login_message(self) -> str:
  26.         """获取登录消息文本。
  27.         返回:
  28.             str: 登录消息的文本内容。
  29.         """
  30.         return self.__base.base_find_element(self.__login_message).text
  31.     def __enter_username(self, username):
  32.         """在用户名输入框中输入指定的用户名。
  33.         参数:
  34.             username (str): 要输入的用户名
  35.         """
  36.         self.__base.base_send_keys(self.__username_input, username)
  37.     def __enter_password(self, password):
  38.         """在密码输入框中输入指定的密码。
  39.         参数:
  40.             password (str): 要输入的密码
  41.         """
  42.         self.__base.base_send_keys(self.__password_input, password)
  43.     def __click_login(self):
  44.         """等待登录按钮可点击,并执行点击操作。"""
  45.         self.__base.base_click(self.__login_button)
复制代码
总结

PO 模式通过将页面的元素和操作封装为对象,使测试代码更清晰、可维护和可复用。

  • 提高代码复用性:多个测试用例可共享同一页面对象。
  • 增强可维护性:页面变化时只需更新页面对象。
  • 提升可读性:测试用例更接近自然语言,如 login_page.enter_username("test")。
根据 PO 模式可以将项目结构划分为三层:

  • 基础层:封装通用方法。
  • 页面对象层:每个页面一个类,封装元素和操作。
  • 测试用例层:调用页面对象完成测试。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册