自动化测试实战 | 搞定 PageObject 设计模式

简介: 在为 UI 页面写测试用例时(比如 Web 页面,移动端页面),测试用例会存在大量元素和操作细节。如何面对当 UI 变化时,测试用例也要跟着变化这个问题?PageObject 设计模式闪亮登场

PageObject 简介

在为 UI 页面写测试用例时(比如 Web 页面,移动端页面),测试用例会存在大量元素和操作细节。如何面对当 UI 变化时,测试用例也要跟着变化这个问题?PageObject 设计模式闪亮登场(由 IT 大佬 Martin Flower 提出)。

使用 UI 自动化测试工具时(Selenium、Appium 等),如果无统一模式进行规范,随着用例的增多会变得难以维护,而 PageObject 让自动化脚本井井有序,将 page 单独维护并封装细节,可以使 testcase 更稳健,不需要大改动。

PageObject 使用

具体做法:把元素信息和操作细节封装到 Page 类中,在测试用例上调用 Page 对象(PageObject),比如存在一个功能“选取相册标题”,需要为之建立函数selectAblumWithTitle(),函数内部是操作细节findElementsWithClass('album')等:

以选“取相册标题”举例,伪代码如下:

selectAblumWithTitle() {
    #选取相册
    findElementsWithClass('album')
    #选取相册标题
    findElementsWithClass('title-field')
    #返回标题内容
    return getText()

}

PageObject 的主要原则是提供一个简单接口 (或者函数,比如上述的 selectAblumWithTitle ),让调用者在页面上可以做任何操作,点击页面元素,在输入框输入内容等。因此,如果要访问一个文本字段,Page Object 应该有获取和返回字符串的方法。Page Object 应该封装对数据的操作细节,比如查找元素和点击元素。当页面元素改动时,应该只改变 Page 类中的内容,不需要改变调用它的地方。

不要为每个 UI 页面都创建一个 page 类,应该只为页面中重要的元素创建 page 类。比如,一个页面显示多个相册,应该创建一个相册列表 page object,它包含许多相册 page object。如果某些复杂 UI 的层次结构只是用来组织 UI,那么它就不应该出现在 page object 中。page object 的目的是通过给页面建模,从而对应用程序的使用者变得有意义:

如果你想导航到另一个页面,初始 page 对象应当 return 另一个 page 对象,比如点击注册,进入注册页面,在代码中就应该 return Register()。如果想获取页面信息,可以 return 基本类型(字符串、日期)。

建议不要在 page object 中放断言。应该去测 page object,而不是让 page object 自己测自己,page object 的责任是提供页面的状态信息。这里仅用 HTML 描述 Page Object,这种模式还可以用来隐藏 Java swing UI 细节,它可用于所有 UI 框架。

PageObject 六大原则

Selenium 针对 PageObject 的核心思想凝聚出了六大原则,掌握六大原则精髓,才可以进行 PageObject 最佳实践演练:

  1. 公共方法代表页面提供的服务
  2. 不要暴露页面细节
  3. 不要把断言和操作细节混用
  4. 方法可以 return 到新打开的页面
  5. 不要把整页内容都放到PO 中
  6. 相同的行为会产生不同的结果,可以封装不同结果

下面,对上述六大原则进行更详细的实操解释:

  1. 原则一:要封装页面中的功能(或者服务),比如点击页面中的元素,可以进入到新的页面,于是,可以为这个服务封装方法“进入新页面”。
  2. 原则二:封装细节,对外只提供方法名(或者接口)。
  3. 原则三:封装的操作细节中不要使用断言,把断言放到单独的模块中,比如 testcase。
  4. 原则四:点击一个按钮会开启新的页面,可以用 return 方法表示跳转,比如return MainPage()表示跳转到新的PO:MainPage。
  5. 原则五:只为页面中重要的元素进行 PO 设计,舍弃不重要的内容。
  6. 原则六:一个动作可能产生不同结果,比如点击按钮后,可能点击成功,也可能点击失败,为两种结果

封装两个方法,click_success和click_error。

基于企业微信的 PO 实战案例

以企业微信首页为例,企业微信首页有二个主要功能:立即注册和企业登录。
企业微信网址:https://work.weixin.qq.com/

  • Index 页面

点击企业登录可以进入登录页面,在页面可以扫码登录和企业注册。

  • Login 页面

点击企业注册可以进入注册页面,在页面可以输入相关信息进行注册。

  • Register 页面

用 Page Object 原则为页面建模,这里涉及三个页面:首页,登录,注册。在代码中创建对应的三个类Inde,Login,Register:

  • 登陆页⾯提供 login findPassword 功能

    • Login类 + login findPassword⽅法
  • 登录页⾯内的元素有多少并不关⼼,隐藏内部界⾯控件
  • 登录成功和失败会分别返回不同的页⾯

    • findPassword
    • loginSuccess
    • loginFail
  • 通过⽅法返回值判断登录是否符合预期
    UML 图

实战代码

目录结构

BasePage 是所有 page object 的父类,它为子类提供公共的方法,比如下面的 BasePage 提供初始化 driver 和退出 driver,代码中在 base_page 模块的 BasePage 类中使用 init 初始方法进行初始化操作,包括 driver 的复用,driver 的赋值,全局等待的设置(隐式等待)等等:

from time import sleep
from selenium import webdriver
from selenium.webdriver.remote.webdriver import WebDriver


class BasePage:
    def __init__(self, driver: WebDriver = None):
        #此处对driver进行复用,如果不存在driver,就构造一个新的
        if driver is None:
            # Index页面需要用,首次使用时构造新driver
            self._driver = webdriver.Chrome()
            # 设置隐式等待时间
            self._driver.implicitly_wait(3)
            # 访问网页
            self._driver.get(self._base_url)
        else:
            # Login与Register等页面需要用这个方法,避免重复构造driver
            self._driver = driver

    def close(self):
        sleep(20)
        self._driver.quit()

Index 是企业微信首页的 page object,它存在两个方法,进入注册 page object 和进入登陆 page object,这里 return 方法返回 page object 实现了页面跳转,比如:goto_register方法return Register,实现从首页跳转到注册页:

from selenium.webdriver.common.by import By

from test_selenium.page.base_page import BasePage
from test_selenium.page.login import Login
from test_selenium.page.register import Register


class Index(BasePage):
    _base_url = "https://work.weixin.qq.com/"
    # 进入注册页面
    def goto_register(self):
        self._driver.find_element(By.LINK_TEXT, "立即注册").click()
        # 创建Register实例后,可调用Register中的方法
        return Register(self._driver)
    # 进入登录页面
    def goto_login(self):
        self._driver.find_element(By.LINK_TEXT, "企业登录").click()
        # 创建Login实例后,可调用Login中的方法
        return Login(self._driver)

Login 是登录页面的 page object,主要功能有:进入注册页面,扫描二维码,因此创建两个方法代表两个功能:scan_qrcode和goto_registry。代码跟上面相似,不过多介绍:

from selenium.webdriver.common.by import By
from test_selenium.page.base_page import BasePage
from test_selenium.page.register import Register

class Login(BasePage):
    # 扫描二维码
    def scan_qrcode(self):
        pass
    # 进入注册页面
    def goto_registry(self):
        self._driver.find_element(By.LINK_TEXT, "企业注册").click()
        return Register(self._driver)

Register 是注册页面的 page object,主要功能是填写正确注册信息,当填写错误时,返回错误信息。register 方法实现了正确的表格填写,当填写完毕时返回自身(页面还停留在注册页)。get_error_message 方法实现了错误填写的情况,如果填写错误,就收集错误内容并返回:

from selenium.webdriver.common.by import By
from test_selenium.page.base_page import BasePage


class Register(BasePage):
    # 填写注册信息,此处只填写了部分信息,并没有填写完全
    def register(self, corpname):
        # 进行表格填写
        self._driver.find_element(By.ID, "corp_name").send_keys(corpname)
        self._driver.find_element(By.ID, "submit_btn").click()
        # 填写完毕,停留在注册页,可继续调用Register内的方法 
        return self
    #填写错误时,返回错误信息
    def get_error_message(self):
        # 收集错误信息并返回
        result=[]
        for element in self._driver.find_elements(By.CSS_SELECTOR, ".js_error_msg"):
            result.append(element.text)

        return result

test_index 模块是对上述功能的测试,它独立于 page 类,在 TestIndex 类中只需要调用 page 类提供的方法即可,比如下面对注册页及登陆页的测试使用了 test_register 和 test_login 方法:

from test_selenium.page.index import Index


class TestIndex:
    # 所有步骤前的初始化
    def setup(self):
        self.index = Index()
    # 对注册功能的测试
    def test_register(self):
        # 进入index,然后进入注册页填写信息
        self.index.goto_register().register("霍格沃兹测试学院")
    # 对login功能的测试
    def test_login(self):
        # 从首页进入到注册页
        register_page = self.index.goto_login().goto_registry()\
            .register("测吧(北京)科技有限公司")
        # 对填写结果进行断言,是否填写成功或者填写失败
        assert "请选择" in "|".join(register_page.get_error_message())
    # 关闭driver
    def teardown(self):
        self.index.close()

更多技术文章分享及测试资料点此获取

相关文章
|
2月前
|
数据采集 JSON JavaScript
Cypress 插件实战:让测试更稳定,不再“偶尔掉链子”
本文分享如何通过自定义Cypress插件解决测试不稳定的痛点。插件可实现智能等待、数据预处理等能力,替代传统硬性等待,有效减少偶发性失败,提升测试效率和可维护性。文内包含具体实现方法与最佳实践。
|
3月前
|
存储 关系型数据库 测试技术
玩转n8n测试自动化:核心节点详解与测试实战指南
n8n中节点是自动化测试的核心,涵盖触发器、数据操作、逻辑控制和工具节点。通过组合节点,测试工程师可构建高效、智能的测试流程,提升测试自动化能力。
|
3月前
|
Web App开发 前端开发 JavaScript
Playwright极速UI自动化实战指南
Playwright告别Selenium痛点,以智能等待、强大选择器、网络拦截与多设备模拟四大利器,提升自动化效率与稳定性。本文通过实战代码详解其加速秘籍,助你构建高效、可靠的UI测试方案。
|
4月前
|
Web App开发 人工智能 JavaScript
主流自动化测试框架的技术解析与实战指南
本内容深入解析主流测试框架Playwright、Selenium与Cypress的核心架构与适用场景,对比其在SPA测试、CI/CD、跨浏览器兼容性等方面的表现。同时探讨Playwright在AI增强测试、录制回放、企业部署等领域的实战优势,以及Selenium在老旧系统和IE兼容性中的坚守场景。结合六大典型场景,提供技术选型决策指南,并展望AI赋能下的未来测试体系。
|
2月前
|
弹性计算 人工智能 前端开发
在阿里云ECS上部署n8n自动化工作流:U2实例实战
本文介绍如何在阿里云ECS的u2i/u2a实例上部署开源工作流自动化平台n8n,利用Docker快速搭建并配置定时任务,实现如每日抓取MuleRun新AI Agent并推送通知等自动化流程。内容涵盖环境准备、安全组设置、实战案例与优化建议,助力高效构建低维护成本的自动化系统。
459 5
|
2月前
|
人工智能 自然语言处理 JavaScript
Playwright MCP在UI回归测试中的实战:构建AI自主测试智能体
Playwright MCP结合AI智能体,革新UI回归测试:通过自然语言驱动浏览器操作,降低脚本编写门槛,提升测试效率与覆盖范围。借助快照解析、智能定位与Jira等工具集成,实现从需求描述到自动化执行的闭环,推动测试迈向智能化、民主化新阶段。
|
2月前
|
数据采集 运维 监控
爬虫与自动化技术深度解析:从数据采集到智能运维的完整实战指南
本文系统解析爬虫与自动化核心技术,涵盖HTTP请求、数据解析、分布式架构及反爬策略,结合Scrapy、Selenium等框架实战,助力构建高效、稳定、合规的数据采集系统。
爬虫与自动化技术深度解析:从数据采集到智能运维的完整实战指南
|
4月前
|
人工智能 缓存 测试技术
Playwright进阶指南 (6) | 自动化测试实战
2025企业级测试解决方案全面解析:从单元测试到千级并发,构建高可用测试体系。结合Playwright智能工具,解决传统测试维护成本高、环境依赖强、执行效率低等痛点,提升测试成功率,内容从测试架构设计、电商系统实战框架、高级测试策略、Docker化部署、CI/CD集成及AI测试应用,助力测试工程师掌握前沿技术,打造高效稳定的测试流程。
Playwright进阶指南 (6) | 自动化测试实战
|
3月前
|
设计模式 人工智能 算法
基于多设计模式的状态扭转设计:策略模式与责任链模式的实战应用
接下来,我会结合实战案例,聊聊如何用「策略模式 + 责任链模式」构建灵活可扩展的状态引擎,让抽奖系统的状态管理从「混乱战场」变成「有序流水线」。
|
3月前
|
人工智能 数据可视化 测试技术
AI 时代 API 自动化测试实战:Postman 断言的核心技巧与实战应用
AI 时代 API 自动化测试实战:Postman 断言的核心技巧与实战应用
471 11