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")