1 理解 PO 模式
2 掌握模型层、动作层、数据层的分离
3 掌握基于原生方法的二次动作自定义操作
4 理解基于原生 find_element 方法的自定义方法实现
5 使用 PO 框架初始化自有项目
一、PO 模式基本介绍
PO模式基本内容
-
页面对象:我们将 APP 中的每个单页可以看做是一个可以单独管理的 对象。
-
PO三层:
- 模型层( 业务层 ):该层属于对象的业务,我们可以看做是脚本执行的入口,因为APP提供给用户的核心就是功能,而执行功能的过程其实就是做事情,我们将做事情说的高大尚一些就是做业务。因此在这层里我们只考虑业务执行的先后顺序,而不会去考虑这些业务是如何实现的。
- 动作层( control 层 ):动作层按着字面意思就是具体的操作过程,例如结算订单算是一个业务,而在这个业务的执行过程中我们需要用到一些具体的 点击动作,所以我们就将这业务执行时会用到的动作单独的拿出去,从而生成对应业务层的动作代码。
- 数据层( data 层 ):数据就是我们在实现业务时需要用到的测试数据,例如我们测试登录的业务,那么我们就有可能输入用户名和密码等数据,而为了不让数据耦合到我们的代码当中,所以我们也将它们单独的抽离出来,这样将来需要修改测试数据的时候我们完成不需要苦恼会不会因为动了某个数据而让整个测试脚本出错。
-
PO 模型工作原理描述:
依据上述的介绍,为了方便大家理解将来代码的实现过程,我们基于所谓的三层来梳理一下脚本的执行过程
- 新建 python 项目,定义好我们的 pytest.ini 配置文件,将脚本执行目录存放于 script 目录下【可以自定义】
- 在 script 目录下新建 test_ 类型的脚本文件,一个 test_XXX.py 就是一个对象的模型层【 这里采用 test 是因为习惯,如果追求对应可以自定义为 module_XXX.py 然后在 pytest.ini 做出修改 】
- 然后在当前项目下新建一个 action 包,这个包里去存放对应的动作脚本,便于将来 module 层可以进行调用
- 最后再在当前项目下新建一个 data 包,这个包里去存放对应的数据存放文件,便于将来 module 层可以进行使用
- 有了上述的包和文件之后,我们只需要在 pycharm 自带的命令行工具中执行 pytest 命令,他会以 script 目录下的module 为入口执行对应的脚本,然后在该脚本执行的时候就会有可能用到对应的 action 和 data 这样一个完整的对象运行过程中就产生了。
PO 模式优缺点
- 优点:有了PO之后,我们可以很清晰的以APP界面为单位来管理我们的测试脚本,同时每个界面我们又拆分成了三层,那么将来在后期维护的过程中我们可以直接准备的定位到某一层进行维护,这样会让我们整个自动化测试过程显得更加清晰和合理
- 缺点:如果没有 PO 那么我们就可以面向过程,想要做哪个模块或者遇到了哪个问题就可以直接写代码来解决这个问题,不用去思考写完之后会怎么样,这种做法对于小的单一模块来说是没有任何问题的,但是我们APP本身是由很多个单一功能组合而成的产品。因此当需要自动化测试的功能越来越多时,传统的做法就显示笨拙繁琐不利于维护。
二、 PO 模式下通用动作封装
1、通用动作封装前言
我们采用 PO 思想的做法就是将本来写在一个脚本上的代码分离成不同的层,在 module 里写业务,在action 里动作,在data 里写数据。在这个过程中我们应该会发现一些动作有可能会在不同对象的模型中被调用,也有可能会在当前对象模型中被多次调用,所以这个时候我们就思考是否可以将这些通用的,常用的动作单独的定义封装和简化。因此就有了我们现在的议题:通用动作封装,方便大家理解过程我们从最初的所有代码都写在一起开始研究
2、无PO模式
# -*- coding=utf-8 -*-
from appium import webdriver
class TestDemo:
def setup(self):
desired_caps = dict()
desired_caps["platformName"] = "android"
desired_caps["platformVersion"] = "5.1.1"
desired_caps["deviceName"] = "*****"
desired_caps["appPackage"] = "com.android.settings"
desired_caps["appActivity"] = ".Settings"
desired_caps["resetKeyboard"] = True
desired_caps["unicodeKeyboard"] = True
self.driver = webdriver.Remote("http://localhost:4723/wd/hub", desired_caps)
def test_seach(self):
self.driver.find_element_by_id( "com.android.settings:id/search" ).click()
self.driver.find_element_by_id("android:id/search_src_text").send_keys( "我是中文" )
self.driver.find_element_by_class_name( "android.widget.ImageButton" ).click()
这个过程中我们将所有的操作都放在一个脚本中,此时我们说不利用管理和维护,因此我们选择了分离。例如将动作和模型进行分离
3、分离了动作和模型
- 模型层代码
# -*- coding=utf-8 -*-
from base import initDriver
from page.page_search import searchPageAction
class TestDemo:
def setup(self):
self.driver = initDriver()
self.searchpage = searchPageAction(self.driver)
def test_seach(self):
self.searchpage.click_search()
self.searchpage.input_value()
self.searchpage.click_back()
此时代码里已经没有了具体查找元素的代码,有的只是类似于 click_back() click_search() 这种明显自定义的动作,而这些之所以能用就是因为在最开始的时候我们导入过 from page.page_search..... 这段代码,所以我们将动作拿了出去
- 动作
# -*- coding=utf-8 -*-
''' 在这个页面当中去存放serach功能测试时需要用到的所有动作 '''
class searchPageAction:
def __init__(self,driver):
self.driver = driver
# 点击放大镜
def click_search(self):
self.driver.find_element_by_id("com.android.settings:id/search").click()
# 输入文字
def input_value(self):
self.driver.find_element_by_id("android:id/search_src_text").send_keys("我是中文")
# 点击回退
def click_back(self):
self.driver.find_element_by_class_name("android.widget.ImageButton").click()
这里就是模型层对应的动作代码存放位置,在它里面我们上体的去找到某个元素然后进行相应的操作,将来可以让模型层直接使用
4、通用动作封装
# -*- coding=utf-8 -*-
class BaseAction:
def __init__(self,abc):
self.driver = abc
# 自定义一个元素查找方法
def find_element(self, feature):
"""
依据用户传入的元素信息特征,然后返回当前用户想要查找元素
:param feature: 元组类型,包含用户希望的查找方式,及该方式对应的值
:return: 返回当前用户查找的元素
"""
return self.driver.find_element(feature[0], feature[1])
# 自定义一个元素点击的方法【 这个方法无论是在当前页面还是其它页面都通用 】
def click(self, feature):
"""
依据用户传入的元素特征 对其实现点击的操作
:param feature: 元素的信息元组
:return:none
"""
self.find_element(feature).click()
# 自定义一个函数实现对具体元素进行值的输入
def input_txt(self, feature, value):
"""
依据用户传入的元素特征,找到对应的元素,然后在它里面输入我们的传入的 value值
:param feature: 元组类型,表示元素的特征
:param value: 用户在元素中输入的内容
:return: none
"""
self.find_element(feature).send_keys(value)
所谓通用动作的封装在我们的PO中就是将一些通用的动作进行一次提取,然后基于原生的方法自定义成我们自已的方法,将他们统一的放置于一个基类中,然后将来整个 APP 的每个页面对象都可以直接导入使用。如上述的代码中可以发现,我们自定义了三个通用的动作 。
三、获取元素方法二次封装
1、元素查找方法二次封装前言
我们知道 webdriver 库中自带了很多的定位元素的方法,而查找元素这个操作是我们在自动化脚本书写里经常用到的,原生的方法在使用的时候可能会有些不统一,不方便,所以我们就选择在原生的基础之上按着我们的规则让他可以更加好用和通用,最终也放置于我们PO框架的基类之中。
2、代码实现
# 自定义一个函数,专门用来帮助我们去获取需要的元素,
# feature = By.XPATH,"//*[@text='显示']"
def get_element(self,feature):
"""
通过使用者传入的元素特征 feature ,返回使用者要想查找的元素
:param feature: 该参数是元组类型,包含了选择元素的方式关键字,和该方式需要的数据值
:return: 返回的是一个元素对象
"""
wait = WebDriverWait( self.driver,5,1 )
# 因为get_element这个自定义的方法不仅仅是只能处理 xpath ,它还可以处理 id class_name
# 所以我们如果想在 get_element 方法中应用 install_xpath 方法,就必须先判断当前的get_element
# 走的是不是 xpath 选元素
# 问题:想办法识别出当前get_element 在被调用的时候走的是不是 xpath
by = feature[0]
value = feature[1]
if by == By.XPATH:
value = self.install_xpath(feature[1])
return wait.until(lambda x: x.find_element(by, value))
# 自定义一个函数,专门用于给我们组装 xpath 路径
def install_xpath(self,user_path):
"""
依据用户传入的 user_path ,返回一个可用的 xpath
:param user_path: 用户传入,要求是字符串或者元组
:return: 返回的是一个字符串,为最终可用的 xpath
"""
res_xpath = ""
start_xpath = "//*["
end_xpath = "]"
if isinstance(user_path, str):
if user_path.startswith("//*["):
return user_path
tmp_list1 = user_path.split(",")
if len(tmp_list1) == 2:
res_xpath = "contains(@%s,'%s')" % (tmp_list1[0], tmp_list1[1])
elif len(tmp_list1) == 3:
res_xpath = "@%s='%s'" % (tmp_list1[0], tmp_list1[1])
elif isinstance(user_path, tuple):
for item in user_path:
tmp_list2 = item.split(",")
if len(tmp_list2) == 2:
res_xpath += "contains(@%s,'%s') and " % (tmp_list2[0], tmp_list2[1])
elif len(tmp_list2) == 3:
res_xpath += "@%s='%s' and " % (tmp_list2[0], tmp_list2[1])
and_index = res_xpath.rfind(" and ")
res_xpath = res_xpath[0:and_index]
else:
print("请按规则使用")
return start_xpath + res_xpath + end_xpath
具体的规则和实现过程,这里没有做详细说明
四、 PO 模式框架初始化项目
如何利用 PO 初始化自有项目
模型层
# -*- coding=utf-8 -*-
from base.getdriver import initdriver
# from action.action_search import Searchpageaction
from action.action import Action
class TestDemo:
def setup(self):
self.driver = initdriver()
# self.searchAction = Searchpageaction(self.driver)
self.action = Action(self.driver)
# 在我们当前的 search 界面里有一个搜索功能要测试
def test_search(self):
# 1 执行搜索动作
self.action.initSearchAction().click_search()
# 2 执行输入文字动作
self.action.initSearchAction().input_value("张三")
# 3 执行回退动作
self.action.initSearchAction().click_back()
动作层
# -*- coding=utf-8 -*-
from selenium.webdriver.common.by import By
from base.baseaction import Baseaction
class Searchpageaction(Baseaction):
# 在这个位置上将当前动作中需要用到的所有元素信息都提前定义好
search_feature = By.ID, "com.android.settings:id/search"
input_feature = By.ID, "android:id/search_src_text"
back_feature = By.CLASS_NAME, "android.widget.ImageButton"
# 定义一个点击搜索按钮的动作
def click_search(self):
self.syy_click( self.search_feature )
# 定义一个输入文字的动作
def input_value(self,value):
self.syy_input( self.input_feature,value )
# 定义一个点击回退的动作
def click_back(self):
self.syy_click( self.back_feature )
注意:这里没有列出我们 base 里的代码