PO模式详解
PO模型
前言
PO模型是:Page Object Model的简写 页面对象模型。
作用:就是把测试页面和测试脚本进行分离,即把页面封装成类,供测试脚本进行调用。
分层机制,让不同层去做不同类型的事情,让代码结构清晰,增加复用性。
PO设计模式是Selenium自动化测试中最佳的设计模式之一,主要体现在对界面交互细节的封装。
PO是什么?
1、页面对象模型(PO)是一种设计模式,用来管理维护一组web元素的对象库。
2、在PO下,应用程序的每一个页面都有一个对应的page class。
3、每一个page class维护着该web页的元素集和操作这些元素的方法。
4、page class中的方法命名最好根据对应的业务场景进行命名。
PO的优势?
1、PO提供了一种业务流程与页面元素操作分离的模式,这使得测试代码变得更加清晰。
2、页面对象与用例分离,使得我们更好的复用对象。
3、可复用的页面方法代码会变得更加优化。
4、更加有效的命名方式使得我们更加清晰的知道方法所操作的UI元素。
Page Object模式具有以下几个优点
该观点来自 《Selenium自动化测试——基于Python语言》
①抽象出对象可以最大程度地降低开发人员修改页面代码对测试的影响, 所以, 你仅需要对页面对象进行调整, 而对测试没有影响;
②可以在多个测试用例中复用一部分测试代码;
③测试代码变得更易读、 灵活、 可维护。
Page Object模式图
- basepage ——selenium的基类,对selenium的方法进行封装
- pageelements——页面元素,把页面元素单独提取出来,放入一个文件中
- searchpage ——页面对象类,把selenium方法和页面元素进行整合
- testcase ——使用pytest对整合的searchpage进行测试用例编写
总结:通过上图我们可以看出,通过POM模型思想,我们把:
- selenium方法
- 页面元素
- 页面对象
- 测试用例
以上四种代码主体进行了拆分,虽然在用例很少的情况下做会增加代码,但是当用例多的时候意义很大,代码量会在用例增加的时候显著减少。我们维护代码变得更加直观明显,代码可读性也变得比工厂模式强很多,代码复用率也极大的得到了提高。
不使用PO设计会出现以下几种情况
复用性不太好,扩展性不好,易读性差,不好维护,UI界面频繁的项目维护起来比较麻烦。
PO模式的优缺点
优点:
提高代码的可读性。
减少了代码的重复。
提高代码的可维护性,特别是针对UI界面频繁的项目。
缺点:
造成项目结构比较复杂,因为是根据流程进行了模块化处理。
代码
代码结构:
base层
base.py
import allure from selenium.webdriver.support.wait import WebDriverWait from tools.get_log import GetLog log = GetLog.get_logger() class Base: # 初始化 def __init__(self, driver): log.info("正在初始化driver: {}".format(driver)) """解决driver""" self.driver = driver # 查找 方法封装 def base_find(self, loc, timeout=30, poll=0.5): """ :param loc: 格式为列表或元祖,内容:元素定位信息使用By类 :param timeout: 查找元素超时时间,默认 30秒 :param poll: 查找元素频率 默认为0.5 :return: 元素 """ log.info("正在查找元素:{}".format(loc)) return (WebDriverWait(self.driver, timeout=timeout, poll_frequency=poll).until(lambda x: x.find_element(*loc))) # 输入 方法封装 def base_input(self, loc, value): """ :param loc: 元素的定位信息 :param value: 要输入的值 """ # 1. 获取元素 el = self.base_find(loc) # 2. 清空操作 log.info("正在对:{} 元素执行清空操作!".format(loc)) el.clear() # 3. 输入操作 log.info("正在对:{} 元素执行输入:{} 操作!".format(loc, value)) el.send_keys(value) # 点击 方法封装 def base_click(self, loc): """ :param loc: 元素定位信息 """ log.info("正在对:{} 元素执行点击操作!".format(loc)) # 获取元素并点击 self.base_find(loc).click() # 获取 元素文本 def base_get_text(self, loc): """ :param loc: 元素定位信息 :return: 返回元素的文本值 """ log.info("正在对:{} 元素获取文本操作!,获取的文本值:{}".format(loc, self.base_find(loc).text)) return self.base_find(loc).text # 截图 def base_get_img(self): # 1. 调用截图方法 self.driver.get_screenshot_as_file("./image/err.png") # 2. 调用图片写入报告方法 self.__base_write_img() # 将图片写入报告方法(私有) def __base_write_img(self): # 1. 获取图片文件流 with open("./image/err.png", "rb") as f: # 2. 调用allure.attach附加方法 allure.attach("错误原因:", f.read(), allure.attach_type.PNG)
web_base.py
from time import sleep from selenium.webdriver.common.by import By from base.base import Base from tools.get_log import GetLog log = GetLog.get_logger() class WebBase(Base): """以下为你web项目专属方法""" # 根据显示文本点击指定元素 def web_base_click_element(self, placeholder_text, click_text): log.info("正在调用web专属点击封装方法") # 1. 点击复选框 loc = By.CSS_SELECTOR, "[placeholder='{}']".format(placeholder_text) self.base_click(loc) # 2. 暂停 sleep(1) # 3. 点击包含显示文本的元素 loc = By.XPATH, "//*[text()='{}']".format(click_text) self.base_click(loc) # 判断页面是否包含指定元素 def web_base_is_exist(self, text): log.info("正在调用查找页面是否存在指定元素:{} 方法".format(text)) # 1. 组装元素配置信息 loc = By.XPATH, "//*[text()='{}']".format(text) # 2. 找元素 try: # 1. 找元素 修改查找元素时间 3 self.base_find(loc, timeout=3) # 2. 输出找到信息 print("找到:{} 元素啦!".format(loc)) # 3. 返回 True return True except: # 1. 输出未找到信息 print("没有找到:{} 元素!".format(loc)) # 2. 返回False return False
app_base.py
from selenium.webdriver.common.by import By from selenium.common.exceptions import NoSuchElementException from base.base import Base from tools.get_log import GetLog from time import sleep log = GetLog.get_logger() class AppBase(Base): # 1. 查找页面是否存在指定元素 def app_base_is_exist(self, loc): try: # 1. 调用查找方法 -> 3 self.base_find(loc, timeout=3) log.info("在app页面中找到指定元素!") # 2. 输出信息 print("找到:{}元素啦!".format(loc)) # 3. 返回True return True except: log.error("没有在app页面中找到指定元素!") # 1. 输出信息 print("未找到:{}元素!".format(loc)) # 2. 返回False return False # 2. 从右向左滑动屏幕 def app_base_right_wipe_left(self, loc_area, click_text): log.info("正在调用从右向左滑动屏幕方法") # 1. 查找区域元素 el = self.base_find(loc_area) # 2. 获取区域元素的位置 y坐标点 y = el.location.get("y") # 3. 获取区域元素宽高 width = el.size.get("width") height = el.size.get("height") # 4. 计算 start_x, start_y, end_x, end_y start_x = width * 0.8 start_y = y + height * 0.5 end_x = width * 0.2 end_y = y + height * 0.5 # 组合频道元素配置信息 # loc = By.XPATH, "//*[@class='android.widget.HorizontalScrollView']//*[contains(@text,'{}')]".format(click_text) loc = By.XPATH, "//android.widget.HorizontalScrollView/*[contains(@text,'{}')]".format(click_text) # 5. 循环操作 while True: # 1. 获取当前屏幕页面结构 page_source = self.driver.page_source # 2. 捕获异常 try: sleep(2) # 1. 查找元素 el = self.base_find(loc, timeout=3) # 2. 输出提示信息 print("找到:{} 元素啦!".format(loc)) sleep(2) # 3. 点击元素 el.click() # 4. 跳出循环 break # 3. 处理异常 except: # 1. 输出提示信息 print("未找到:{}元素!".format(loc)) # 2. 滑动屏幕 self.driver.swipe(start_x, start_y, end_x, end_y, duration=2000) # 4. 判断是否为最后一页 if page_source == self.driver.page_source: # 1. 输出提示信息 print("滑到最后一屏幕,未到找元素!") # 2. 抛出未找到元素异常 raise NoSuchElementException # 3. 从下向上滑动屏幕 def app_base_down_wipe_up(self, loc_area, click_text): log.info("正在调用从下向上滑动屏幕方法") # 1. 查找区域元素 el = self.base_find(loc_area) # 2. 获取区域元素宽高 width = el.size.get("width") height = el.size.get("height") # 3. 计算 start_x, start_y, end_x, end_y start_x = width * 0.5 start_y = height * 0.8 end_x = width * 0.5 end_y = height * 0.2 # 组合频道元素配置信息 loc = By.XPATH, "//*[@bounds='[0,520][1440,2288]']//*[contains(@text,'{}')]".format(click_text) # 5. 循环操作 while True: # 1. 获取当前屏幕页面结构 page_source = self.driver.page_source # 2. 捕获异常 try: # 1. 查找元素 el = self.base_find(loc, timeout=3) # 2. 输出提示信息 print("找到:{} 元素啦!,文章标题为:{}".format(loc, el.text)) # 3. 点击元素 el.click() # 4. 跳出循环 break # 3. 处理异常 except: # 1. 输出提示信息 print("未找到:{}元素!".format(loc)) # 2. 滑动屏幕 self.driver.swipe(start_x, start_y, end_x, end_y, duration=2000) # 4. 判断是否为最后一页 if page_source == self.driver.page_source: # 1. 输出提示信息 print("滑到最后一屏幕,未到找元素!") # 2. 抛出未找到元素异常 raise NoSuchElementException
page层
page_mis_login.py
from base.web_base import WebBase import page from tools.get_log import GetLog log = GetLog.get_logger() class PageMisLogin(WebBase): # 1. 输入用户名 def page_input_username(self, username): self.base_input(page.mis_username, username) # 2. 输入密码 def page_input_pwd(self, pwd): self.base_input(page.mis_pwd, pwd) # 3. 点击登录按钮 def page_click_login_btn(self): # 1. 处理js js = "document.getElementById('inp1').disabled=false" self.driver.execute_script(js) # 2. 调用点击操作 self.base_click(page.mis_login_btn) # 4. 获取昵称封装 def page_get_nickname(self): return self.base_get_text(page.mis_nickname) # 5. 组合后台管理登录业务方法 def page_mis_login(self, username, pwd): log.info("正在调用后台管理系统登录业务方法,用户名:{} 密码:{}".format(username, pwd)) self.page_input_username(username) self.page_input_pwd(pwd) self.page_click_login_btn() # 6. 组合后台管理登录业务方法 def page_mis_login_success(self, username="testid", pwd="testpwd123"): log.info("正在调用后台管理系统成功登录依赖方法,用户名:{} 密码:{}".format(username, pwd)) self.page_input_username(username) self.page_input_pwd(pwd) self.page_click_login_btn()
page_mis_audit.py
from base.web_base import WebBase from time import sleep import page from tools.get_log import GetLog log = GetLog.get_logger() class PageMisAudit(WebBase): # 文章id article_id = None # 1. 点击信息管理 def page_click_info_manage(self): # 1. 暂停时间 sleep(1) # 2. 点击信息管理 self.base_click(page.mis_info_manage) # 2. 点击内容审核 def page_click_content_audit(self): # 1. 暂停时间 sleep(1) # 2. 点击内容审核 self.base_click(page.mis_content_audit) # 3. 输入文章标题 def page_input_title(self, title): self.base_input(page.mis_title, title) # 4. 输入文章频道 def page_input_channel(self, channel): self.base_input(page.mis_channel, channel) # 5. 选择状态 def page_click_status(self, placeholder_text="请选择状态", click_text="待审核"): self.web_base_click_element(placeholder_text, click_text) # 6. 点击查询按钮 def page_click_find(self): # 1. 点击查询按钮 self.base_click(page.mis_find) # 2. 暂停时间 sleep(2) # 7. 获取文章id def page_get_article_id(self): return self.base_get_text(page.mis_article_id) # 8. 点击通过 def page_click_pass_btn(self): self.base_click(page.mis_pass) # 9. 点击确认 def page_click_confirm_pass(self): # 1. 暂停时间 sleep(1) # 点击确认 self.base_click(page.mis_confirm_pass) # 10. 组合发布文章业务方法 def page_mis_audit(self, title, channel): log.info("正在调用审核文章业务方法,title: {} channel:{}".format(title, channel)) self.page_click_info_manage() self.page_click_content_audit() self.page_input_title(title) self.page_input_channel(channel) self.page_click_status() self.page_click_find() self.article_id = self.page_get_article_id() print("获取的文章id为:", self.article_id) self.page_click_pass_btn() self.page_click_confirm_pass() # 11. 组装断言业务操作方法 def page_assert_audit(self): log.info("正在调用断言业务操作方法") # 1. 暂停3秒 sleep(3) # 2. 修改状态 ->审核通过 self.page_click_status(click_text="审核通过") # 3. 点击查询按钮 self.page_click_find() # 4. 判断当前页面是否存在指定元素 并 返回结果 return self.web_base_is_exist(self.article_id)
page_app_login.py
from base.app_base import AppBase import page from time import sleep from tools.get_log import GetLog log = GetLog.get_logger() class PageAppLogin(AppBase): # 1. 输入手机号 def page_input_phone(self, phone): self.base_input(page.app_phone, phone) # 2. 输入验证码 def page_input_code(self, code): self.base_input(page.app_code, code) # 3. 点击登录按钮 def page_click_login_btn(self): # 建议等待1-2秒 sleep(2) self.base_click(page.app_login_btn) # 4. 判断页面是否存在 我的菜单 def page_is_login_success(self): return self.app_base_is_exist(page.app_me) # 5. 组合登录业务方法 def page_app_login(self, phone, code): log.info("正在调用app应用登录业务方法 手机号:{} 验证码: {}".format(phone, code)) self.page_input_phone(phone) self.page_input_code(code) self.page_click_login_btn() # 6. 组合登录依赖成功业务方法 def page_app_login_success(self, phone="13812345678", code="246810"): log.info("正在调用app应用登录业务方法 手机号:{} 验证码: {}".format(phone, code)) self.page_input_phone(phone) self.page_input_code(code) self.page_click_login_btn()
page_app_article.py
from base.app_base import AppBase import page from tools.get_log import GetLog log = GetLog.get_logger() class PageAppArticle(AppBase): # 1. 查找频道 def page_click_channel(self, click_text): # 调用 从右向左滑动方法 self.app_base_right_wipe_left(page.app_channel_area, click_text) # 2. 查找文章 def page_click_article(self, title): # 调用 从下向上滑动方法 self.app_base_down_wipe_up(page.app_article_area, click_text=title) # 3. 查找文章业务方法 def page_app_article(self, find_text, title): log.info("正在调用查询文章业务方法 文章频道:{} 文章title:{}".format(find_text, title)) # 1. 调用查找频道 self.page_click_channel(find_text) # 2. 调用查找文章 self.page_click_article(title)
封装统一入口类:page_in.py
from page.page_app_article import PageAppArticle from page.page_app_login import PageAppLogin from page.page_mis_audit import PageMisAudit from page.page_mis_login import PageMisLogin from page.page_mp_article import PageMpArticle from page.page_mp_login import PageMpLogin class PageIn: def __init__(self, driver): self.driver = driver # 获取PageMpLogin对象 def page_get_PageMpLogin(self): return PageMpLogin(self.driver) # 获取PageMpArticle对象 def page_get_PageMpArticle(self): return PageMpArticle(self.driver) # 获取PageMisLogin对象 def page_get_PageMisLogin(self): return PageMisLogin(self.driver) # 获取PageMisAudit对象 def page_get_PageMisAudit(self): return PageMisAudit(self.driver) # 获取PageAppLogin对象 def page_get_PageAppLogin(self): return PageAppLogin(self.driver) # 获取PageAppArticle对象 def page_get_PageAppArticle(self): return PageAppArticle(self.driver)
scripts层
test01_mp_login.py
import pytest from tools.get_log import GetLog from page.page_in import PageIn from tools.get_driver import GetDriver import page from tools.read_yaml import read_yaml log = GetLog.get_logger() class TestMpLogin: # 初始化 def setup_class(self): # 1. 获取driver driver = GetDriver.get_web_driver(page.url_mp) # 2. 通过统一入口类获取PageMpLogin对象 self.mp = PageIn(driver).page_get_PageMpLogin() # 结束 def teardown_class(self): # 调用关闭driver GetDriver.quit_web_driver() # 测试业务方法 @pytest.mark.parametrize("username,code,expect", read_yaml("mp_login.yaml")) def test_mp_login(self, username, code, expect): # 调用登录业务方法 self.mp.page_mp_login(username, code) try: # 断言 assert expect == self.mp.page_get_nickname() except Exception as e: log.error("断言出错,错误信息:{}".format(e)) print("错误原因:", e) # 截图 self.mp.base_get_img() # 抛异常 raise
test02_mp_article.py
import pytest import page from page.page_in import PageIn from tools.get_driver import GetDriver from tools.get_log import GetLog from tools.read_yaml import read_yaml log = GetLog.get_logger() class TestMpArticle: # 1. 初始化 def setup_class(self): # 1. 获取driver driver = GetDriver.get_web_driver(page.url_mp) # 2. 获取统一入口类对象 self.page_in = PageIn(driver) # 3. 获取PageMpLogin对象并调用成功登录依赖方法 self.page_in.page_get_PageMpLogin().page_mp_login_success() # 4. 获取PageMpArticle页面对象 self.article = self.page_in.page_get_PageMpArticle() # 2. 结束 def teardown_class(self): GetDriver.quit_web_driver() # 3. 测试发布文章方法 @pytest.mark.parametrize("title,content,expect,channel", read_yaml("mp_article.yaml")) def test_mp_article(self, title, content, expect, channel): print("发布文章所属频道为:", channel) # 调用发布文章业务方法 self.article.page_mp_article(title, content) try: # 断言 assert expect == self.article.page_get_info() except Exception as e: # 日志 log.error(e) # 截图 self.article.base_get_img() # 抛异常 raise
test05_app_login.py
import pytest from page.page_in import PageIn from tools.get_driver import GetDriver from tools.get_log import GetLog from tools.read_yaml import read_yaml log = GetLog.get_logger() class TestAppLogin: # 1. 初始化 def setup_class(self): # 1. 获取driver driver = GetDriver.get_app_driver() # 2. 通过统一入口对象获取PageAppLogin对象 self.login = PageIn(driver).page_get_PageAppLogin() # 2. 结束 def teardown_class(self): # 关闭driver GetDriver.quit_app_driver() # 3. app登录测试业务方法 @pytest.mark.parametrize("phone,code", read_yaml("app_login.yaml")) def test_app_login(self, phone, code): # 调用app登录业务方法 self.login.page_app_login(phone, code) try: # 断言 assert self.login.page_is_login_success() except Exception as e: # 1. 日志 log.error(e) # 2. 截图 self.login.base_get_img() # 3. 抛异常 raise
test06_app_article.py
import pytest from page.page_in import PageIn from tools.get_driver import GetDriver from tools.get_log import GetLog from tools.read_yaml import read_yaml log = GetLog.get_logger() class TestAppLogin: # 1. 初始化 def setup_class(self): # 1. 获取driver driver = GetDriver.get_app_driver() # 2. 通过统一入口对象获取PageAppLogin对象 self.login = PageIn(driver).page_get_PageAppLogin() # 2. 结束 def teardown_class(self): # 关闭driver GetDriver.quit_app_driver() # 3. app登录测试业务方法 @pytest.mark.parametrize("phone,code", read_yaml("app_login.yaml")) def test_app_login(self, phone, code): # 调用app登录业务方法 self.login.page_app_login(phone, code) try: # 断言 assert self.login.page_is_login_success() except Exception as e: # 1. 日志 log.error(e) # 2. 截图 self.login.base_get_img() # 3. 抛异常 raise
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!