软件测试PO模式

V1:不使用任何设计模式和单元测试框架
V2:使用UnitTest管理用例
V3:使用方法封装的思想,对代码进行优化
V4:采用PO模式的分层思想对代码进行拆分
V5:对PO分层之后的代码继续优化
V6:PO模式深入封装,把共同操作提取封装到父类中,子类直接调用父类的方法

概念

PO是Page Object缩写,是自动化测试项目开发实践的最佳设计模式之一,核心思想是通过对界面元素的封装减少多余代码,并且后期维护 若元素发生变化 只需要调整页面元素封装的代码 提高测试用例的可维护性 可读

PO模式页面分为 三层 对象库层 操作层 业务层

V6代码审计

把共同操作提取封装到父类中,子类直接调用父类的方法,避免代码冗余

对象库层-基类,把定位元素的方法定义在基类中

操作层-基类,把对元素执行输入操作的方法定义在基类中

utils.py 里面封装了 异常处理 get_tip_msg() 方法,前置添加的 DriverUtil
这个DriverUtil中有着get_driver() 获取驱动对象,打开页面 获取浏览器, quit_driver()

关闭浏览器方法,这两个方法在使用引入前就会被调用

utils.py工具类开启关闭异常

完整测试使用

import time
from selenium.webdriver.common.by import By
from selenium import webdriver# 获取弹出框的提示消息
def get_tip_msg():time.sleep(1)msg = DriverUtil.get_driver().find_element(By.ID,"emailB").text # 找这个文本的字保存再 msge中print("msg==", msg)  # 打印语句就后悔输出return msg # 把这个文本拿出去 去异常抛出里面# 工具类:驱动对象的操作
class DriverUtil:# 私有变量 这个值用于下方的判断__driver = None# 获取驱动对象@classmethoddef get_driver(cls):# 先判断私有变量__driver是否为None 是则执行if cls.__driver is None:cls.__driver = webdriver.Chrome()cls.__driver.maximize_window()cls.__driver.implicitly_wait(10)# 打开页面cls.__driver.get("E:/注册B.html")# 直接使用这个方法返回return cls.__driver# 关闭驱动@classmethoddef quit_driver(cls):time.sleep(5) # 停留5秒if cls.__driver is not None:cls.__driver.quit()cls.__driver = None

base包下 新建 base_page.py 存在对象层父类 class BasePage: 其中有两个方法__init__() 使用驱动类拿到打开页面的驱动get_driver()赋值 到driver 变量方便后续使用,find_element() 方法查找页面上的元素定位元素

操作层父类 class BaseHandle:input_text()方法它的作用是先把通过 element 定位到的元素执行清空操作, element.clear() 再使获取到的value 使用element.send_keys(value)添加操作, element实际上就是查找元素的定位

base.page.py对象库层定位清空添加

完整测试使用

# 获取驱动对象
from utils import DriverUtil# 对象库层-父类
class BasePage:def __init__(self):# 使用工具驱动类拿到自己driver赋值给 driverself.driver = DriverUtil.get_driver()# 定位元素def find_element(self, location):# find_element方法来查找页面上的元素。# *location是一个可变参数,用于指定元素的定位方式和值。return self.driver.find_element(*location)# 操作层-父类
class BaseHandle:# 输入文本内容def input_text(self, element, value):element.clear() # 清空element.send_keys(value)

test_login.py 测试使用入口

script 包下新建test_login.py 我更喜欢称之为使用层,在这里执行我们的测试操作,这里前期就需要把驱动类中的 DriverUtil里面有前置打开页面和关闭驱动引入 和 get_tip_msg() 异常方法引入,;LoginProxy类是操作层数据层层传递查找就靠他

from utils import DriverUtil   # 引入utils.py 驱动类
from utils import get_tip_msg  # 异常
from v6.page.login_page import LoginProxy # 操作层传递值
from selenium.webdriver.common.by import By  # 定位

我分2部分解释代码,这里的 TestLogin 放置了 两个前置方法也初始化环境, 在setUp()方法中6行把驱动类的打开页面驱动方法拿到赋值给 driver 使用 8行 将 操作层 LoginProxy的类拿到给值login_proxy 存储使用, tearDown() 拿到的是关闭驱动的方法,但是没有额外操作只是调用这个关闭驱动的方法, 通过在测试前执行打开页面 及执行完关闭页面这样页面就正常运行

class TestLogin(unittest.TestCase):# -> None表示该方法不返回任何值。这是一种类型提示的方式,用于指示该方法不会返回任何内容。# 在setUp方法中,通常不需要返回任何值,因此使用-> None来明确表示这一点def setUp(self) -> None:# 使用工具类拿到自己driver 里面是一些前期的工作self.driver = DriverUtil.get_driver()#  实例化一下 方便后续操作self.login_proxy = LoginProxy()def tearDown(self) -> None:# 拿到关闭的一些操作DriverUtil.quit_driver()

打开页面之后就是需要对页面做具体的操作,原本4 是使用 By.lint_text 找到登录的文本,执行点击操作,这里我方便演示就没有执行, 然后执行 梦开始的地方,**第一步把需要的值传递进去,需要几个地方写值就传递几个,但是每增加一个值,在定位处还有别的查找元素方法就需要多增加一条数据,**这里只写值,传递到 login_proxy类的 login()方法中

全部的重点查找代码全部都在 login**_**page

   # 账号不存在def test01_login_username_not_exist(self):# 找到元素 赋值其中这里是找到登录的按钮然后执行 click() 但这里我就切换演示,还是一个意思self.driver.find_element(By.CSS_SELECTOR,"#userB").send_keys("123")# 使用 login_proxy操作它的logib方法传参进去  1111111self.login_proxy.login("13099999999", "123456","8888")   # 第一步 # 获取错误提示信息,前期已经导入异常方法所以直接使用msg = get_tip_msg()# 断言 如果字符串存在账号不存在那么就异常self.assertIn("账号不存在", msg)

完整测试使用

import unittest# 导入工具类里面写了前置条件这些
from utils import DriverUtil
from utils import get_tip_msg
from v6.page.login_page import LoginProxy
from selenium.webdriver.common.by import Byclass TestLogin(unittest.TestCase):# -> None表示该方法不返回任何值。这是一种类型提示的方式,用于指示该方法不会返回任何内容。# 在setUp方法中,通常不需要返回任何值,因此使用-> None来明确表示这一点def setUp(self) -> None:# 使用工具类拿到自己driver 里面是一些前期的工作self.driver = DriverUtil.get_driver()#  实例化一下 方便后续操作self.login_proxy = LoginProxy()def tearDown(self) -> None:# 拿到关闭的一些操作DriverUtil.quit_driver()# 账号不存在def test01_login_username_not_exist(self):# 找到元素 赋值self.driver.find_element(By.CSS_SELECTOR,"#userB").send_keys("123")# 使用 login_proxy操作它的logib方法传参进去  1111111self.login_proxy.login("13099999999", "123456","8888")# 获取错误提示信息msg = get_tip_msg()# 断言 如果字符串存在账号不存在那么就异常#self.assertIn("账号不存在", msg)# 密码错误def test02_login_pwd_error(self):# 点击首页的‘登录’链接,进入登录页面self.driver.find_element_by_link_text("登录").click()self.login_proxy.login("13012345678", "11112222", "8888")   # 第一步 # 获取错误提示信息msg = get_tip_msg() # 获取到的文本# 断言 文本是这个的话那么就是有错误,,self.assertIn("密码错误", msg) #

login_page.py 操作元素查找

page包下新建login_page.py,里面全部是查找元素处理元素的代码,同时联动base.page.py对象库层的定位实现了查找元素,这部分思路比较多请耐心的审阅代码,里面有3个类 ,作用也不同

逻辑实现

LoginPage使用base.page.py类定位需要操作的元素 放置的是初始的左边再通过find_element传入

这里是第4步层层传递,但是牵扯到了 base.page.py下的 find_element() 定位方法

LoginHandle 继承base.page.py类使用里面对元素的文本添加清空的操作input_text()这里是

第3步已经执行完成,前置到了 base.page.py下的 input_text() 元素添加操作

LoginProxy 第2步操作,把接收到的各个值,传递给上方的 LoginHandle

首先引入需要联动的两个类,BasePage 定位查找代码 BaseHandle清空添加值

from v6.base.base_page import BasePage, BaseHandle

在入口测试类test_login.py 已经是运行了这行代码,所以要找到login_proxy类的login()方法去看,传递了3个值 也就是3个形参,传递几个那么函数的实参也要写几个对应上

self.login_proxy.login("13099999999", "123456","8888")

这里就是数据运行第二部分,在 11 行可以看到数据又被传递给了 login_handle.input_username(username),我们又需要去login_handle类里面去看 input_username(username)这个方法里面的值是如果运行的 第二不结束

# 业务层:将一个或多个元素的操作封装一个具体的业务class LoginProxy:def __init__(self):# 实例化 LoginHandle() 给 login_handle使用self.login_handle = LoginHandle()# 登录def login(self, username, pwd, code):# 形参通过实例化对象login_handle把值传到input_username()中# 把值又传递一层 给了 LoginHandle()下面的方法self. login_handle.input_username(username)   # 第二步 # 密码self.login_handle.input_pwd(pwd)# 验证码self.login_handle.input_code(code)# 登录按钮# self.login_handle.click_login_btn()

LoginHandle继承了文本清空添加的方法,并且5行 代码实例化了上方的类 LoginPage(),后面会讲到,通过 login_page 来操作这个类里面的方法

回到这里面的3个方法中,以input_username(self, username): 举例,里面的username肯定是我们传过来的 13012345678,然后在这个方法中 使用了

self.input_text (self.login_page.find_username(), username)

input_text()是由于继承BaseHandle来的清空添加方法, 它的原始形式是这样的 于是我们把input_username()的形参和原始的进行对比

input_text(self, element,                       value):
input_text (self.login_page.find_username(),   username) login_page.find_username().clear() 清空 回到  login_page类去查找  第三步 
login_page.find_username().send_keys(value) 添加

结果发现是先执行清空再添加,值,到这里 第3步执行完成,去**login****_**page类去查找find_username,观察它的执行逻辑

# 操作层:封装对元素操作的方法
class LoginHandle(BaseHandle):  # 继承 BaseHannledef __init__(self):# 实例化login_page()  后面使用这个对象都通过login_page调用使用上方的方法self.login_page = LoginPage()def input_username(self, username):# input_text是继承来的方法清空添加   find_username()是找到这个元素后# 再回到 base_pase element= find_username 先执行清空,再执行添加# 333333333 回去看base 怎么走的# find_username 定位找值的方法self.input_text (self.login_page.find_username(),  username)def input_pwd(self, pwd):self.input_text(self.login_page.find_pwd(), pwd)def input_code(self, code):self.input_text(self.login_page.find_code(), code)## 点击登录的按钮# def click_login_btn(self):#     self.login_page.find_login_btn().click()

LoginPage类下有find_username()方法,它的逻辑很简单, self.find_element(self.username)

这明显是一个查找位置的,并且传入的值为username坐标, 并且这里的find_element 发现了吗缺少了必要的浏览器对象,dirver,而且使用的继承 BasePage类,它里面就存在一个 find_element() 方法包括 dirver 所以破案,查找左边的这边还利用到了find_element下的find_element()方法,这就是整个数据传递的执行逻辑。

  self.username = (By.ID, "userB")self.find_element(self.username)self.find_element(By.ID, "userB")    # 拼接的最后形式
class LoginPage(BasePage):def __init__(self):super().__init__()  # 调用父类构造方法也就是继承来的BasePage初始化工具驱动类# 对象查询元素的方式# 用户名#self.username = (By.ID, "username")self.username = (By.ID, "userB")# 密码self.pwd = (By.ID,"passwordB")# 验证码 电话号码#self.code = (By.ID, "verify_code")self.code = (By.ID,"telB")# 登录按钮# self.login_btn = (By.NAME, "sbtbutton")def find_username(self):# 44444 找到各类值 添加 username id 位置这些,所以# find_username = find_element(By.ID, "userB")# 然后把值给到下方找值的方法# 54行# self.input_text (self.login_page.find_username(),  username)return self.find_element(self.username)def find_pwd(self):return self.find_element(self.pwd)def find_code(self):return self.find_element(self.code)

完整测试使用

from    selenium.webdriver.common.by import Byfrom v6.base.base_page import BasePage, BaseHandle# 对象库层才是重点 里面放置的是元素的操作这些,查找元素# 对象库层:找到要操作的元素
class LoginPage(BasePage):def __init__(self):super().__init__()  # 调用父类构造方法也就是继承来的BasePage# 对象查询元素的方式# 用户名#self.username = (By.ID, "username")self.username = (By.ID, "userB")# 密码self.pwd = (By.ID,"passwordB")# 验证码 电话号码#self.code = (By.ID, "verify_code")self.code = (By.ID,"telB")# 登录按钮# self.login_btn = (By.NAME, "sbtbutton")def find_username(self):# 44444 找到各类值 添加 username id 位置这些,所以# find_username = find_element(By.ID, "userB")# 然后把值给到下方找值的方法# 54行# self.input_text (self.login_page.find_username(),  username)return self.find_element(self.username) # 其实这里就是base里面的定位方法只是没有把dirver写出来def find_pwd(self):return self.find_element(self.pwd)def find_code(self):return self.find_element(self.code)# 查找点击登录# def find_login_btn(self):#     return self.find_element(self.login_btn)# 操作层:封装对元素操作的方法
class LoginHandle(BaseHandle):  # 继承 BaseHannledef __init__(self):# 实例化对login_page()  后面使用这个对象都通过login_page调用self.login_page = LoginPage()def input_username(self, username):# 使用find_username()方法,但是外围又使用继承来的 BaseHannle方法# input_text是继承来的方法清空添加   find_username()是找到这个元素后# 再回到 base_pase element= find_username 先执行清空,再执行添加6# 然后在清空的继承上 加入 value=username# 333333333 回去看base 怎么走的# find_username 定位找值的方法self.input_text (self.login_page.find_username(),  username)def input_pwd(self, pwd):self.input_text(self.login_page.find_pwd(), pwd)def input_code(self, code):self.input_text(self.login_page.find_code(), code)## 点击登录的按钮# def click_login_btn(self):#     self.login_page.find_login_btn().click()# 业务层:将一个或多个元素的操作封装一个具体的业务
class LoginProxy:def __init__(self):self.login_handle = LoginHandle()# 登录def login(self, username, pwd, code):# 账号 222222# 添加值进去的方法利用实例化对象 login_handle把值传到input_username.....# 把值又传递一层 给了 LoginHandle()下面的方法self.login_handle.input_username(username)# 密码self.login_handle.input_pwd(pwd)# 验证码self.login_handle.input_code(code)# 登录按钮# self.login_handle.click_login_btn()

注意:

如果需要添加新的元素,那么就需要在操作元素这个入口3个类中都添加上对应的代码,包括一个元素的定位坐标 self.username = (By.ID, "userB")

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/8589.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

网页版五子棋——匹配模块(客户端开发)

前一篇文章:网页版五子棋——用户模块(客户端开发)-CSDN博客 目录 前言 一、前后端交互接口设计 二、游戏大厅页面 1.页面代码编写 2.前后端交互代码编写 3.测试获取用户信息功能 结尾 前言 前面文章介绍完了五子棋项目用户模块的代码…

Spring设计模式

设计模式 是一种软件开发中的解决方案,设计原则。目的是使代码具有扩展性,可维护性,可读性,如: 单例模式(Singleton Pattern) Spring IoC 容器默认会将 Bean 创建为单例,保证一个类…

【设计模式】结构型模式(一):适配器模式、装饰器模式

结构型模式(一):适配器模式、装饰器模式 1.适配器模式(Adapter)2.装饰器模式(Decorator)2.1 主要特点2.2 组成部分2.3 示例代码2.3.1 Component 组件2.3.2 ConcreteComponent 具体组件2.3.3 Dec…

Go Energy 跨平台(GUI)应用编译和安装包制作

构建打包 energy cli 平台介绍描述windowNSIS安装包制作工具可通过 energy cli 安装linuxdpkg 命令系统自带macosenergy 仅生成 xxx.app系统自带 安装包制作 config/energy_[os].json是初始化应用时自动生成的应用配置文件,在编译和制作应用安装包时使用 Windows…

【Linux】进程信号全攻略(二)

🌈 个人主页:Zfox_ 🔥 系列专栏:Linux 目录 一:🔥 再谈信号的捕捉 🦋 关于信号捕捉的细节部分(sigaction函数) 二:🔥 穿插话题 - 操作系统是怎么运…

鸿蒙的进击之路

1. 题记: 为什么要写鸿蒙,因为她是华为的,为什么是华为就要写,因为华为背负了国人太多太多的包袱,或点赞或抨击。 我是强烈支持华为的,但我会客观公正地去评价华为的产品,就比如这篇博文&#…

Swagger的介绍和使用方式+常用注解

介绍: 使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面.简单来说就是我们只需要知道使用Swagger可以帮助我们后端生成接口文档 Swagger官网:https://swagger.io/ 因为单独使用Swagger会有些…

FFmpeg 4.3 音视频-多路H265监控录放C++开发十三:将AVFrame转换成AVPacket。视频编码,AVPacket 重要函数,结构体成员学习

前提: 从前面的学习我们知道 AVFrame中是最原始的 视频数据,这一节开始我们需要将这个最原始的视频数据 压缩成 AVPacket数据, 我们前面,将YUV数据或者 RGBA 数据装进入了 AVFrame里面,并且在SDL中显示。 也就是说&…

QinQ VLAN技术

QinQ VLAN技术的主要作用包括扩展VLAN数量、实现私网VLAN透传、提供二层隔离和多租户环境等。以下是对这些作用的详细介绍: 扩展VLAN数量 解决VLAN ID不足问题:QinQ技术通过在原有的802.1Q标签基础上再增加一层802.1Q标签,从而将VLAN数量从40…

【机器学习】24. 聚类-层次式 Hierarchical Clustering

1. 优势和缺点 优点: 无需提前指定集群的数量 通过对树状图进行不同层次的切割,可以得到所需数量的簇。树状图提供了一个有用的可视化-集群过程的可解释的描述树状图可能揭示一个有意义的分类 缺点: 计算复杂度较大, 限制了其在大规模数据…

分析报告、调研报告、工作方案等的提示词

什么是提示词? 提示词的英文是Prompt,是你与人工智能(AI)进行交流的方式。简单来说,提示词就是你给AI的一段文字或问题,AI根据这段文字或问题来生成回应或完成任务。 举个例子:假设你在使用一…

Sentinel通过限流对微服务进行保护

目录 雪崩问题 解决雪崩问题的方法: 我们使用sentinel组件实现微服务的保护 一:下载sentinel 二.启动sentinel 三.访问:localhost:8080 默认的账号和密码都是sentinel 微服务整合sentinel 一.导入sentinel依赖 二.在application.yml配…

第三十篇——微分(上):如何从宏观变化了解微观趋势?

目录 一、背景介绍二、思路&方案三、过程1.思维导图2.文章中经典的句子理解3.学习之后对于投资市场的理解4.通过这篇文章结合我知道的东西我能想到什么? 四、总结五、升华 一、背景介绍 微分给我们带来的思维方式的变化,更加符合现在这个时代对于我…

Springboot项目报错记录

SpringBoot测试报错:Unable to find a SpringBootConfiguration, you need to use Context 该测试类所在测试包test下的包名和类路径java下的包名不一致导致的 引发以下报错 java.lang.IllegalStateException: Unable to find a SpringBootConfiguration, you need…

VTK知识学习(3)-显示圆柱

1、添加显示控件 前台界面 <WindowsFormsHost x:Name"windowHost" Grid.Row"1"/> 构造函数中添加。 private RenderWindowControl renderWindowControl new RenderWindowControl();public MainWindow(){InitializeComponent();windowHost.Child …

《FreeRTOS的配置与临界段》

目录 1.FreeRTOS配置的重要性 2.初学者使用FreeRTOSConfig.h 文件 3.“INCLUDE_”开始的宏 4.FreeRTOS 中断配置和临界段 4.1 中断简介 4.2 中断优先级分组定义 4.3优先级设置 4.4 重要的中断屏蔽寄存器 一、PRIMASK 和 FAULTMASK 寄存器 二、BASEPRI 寄存器 4.5 F…

Vue:模板 MVVM

Vue&#xff1a;模板 & MVVM 模板插值语法指令语法 MVVMdefineProperty数据代理 模板 Vue实例绑定一个容器&#xff0c;想要向容器中填入动态的值&#xff0c;就需要使用模板语法。模板语法分为插值语法和指令语法。 插值语法 插值语法很简单&#xff0c;使用{{}}包含一…

极简实现酷炫动效:Flutter隐式动画指南第三篇自定义Flutter隐式动画

目录 前言 一、TweenAnimationBuilder 二、使用TweenAnimationBuilder实现的一些动画效果 1.调整透明度的动画 2.稍微复杂点的组合动画 3.数字跳动的动画效果 前言 上两节博客分别介绍了Flutter中的隐式动画的基础知识以及使用隐式动画实现的一些动画效果。当系统提供的隐…

熵基ZKTeco考勤机门禁如何重置密码(适用于大多数彩屏门禁机)

公司的一台门禁忘记密码了打不开&#xff0c;找了很久终于找到了密码重置的方法。 1、断电拆机(机器底部的螺丝,将机器从墙上拿下来) 2、插电重启&#xff08;或者杵下底部reset小孔&#xff09; 3、机器屏幕显示被拆除&#xff08;或右上角红色小感叹号闪烁&#xff0c;后者启…

​基于学习的地铁客流动态预测智能调度方法

1 文章信息 文章题为“A Learning Based Intelligent Train RegulationMethod With Dynamic Prediction forthe Metro Passenger Flow”&#xff0c;该文于2023年发表至“IEEE TRANSACTIONS ON INTELLIGENT TRANSPORTATION SYSTEMS”。文章的核心观点是提出了一种基于学习的智…