美文网首页
Page Object 模式很火,UI 自动化测试到底要不要用?

Page Object 模式很火,UI 自动化测试到底要不要用?

作者: 霍格沃兹测试学院 | 来源:发表于2020-04-14 11:17 被阅读0次
    Page Object 模式很火,UI 自动化测试到底要不要用?怎么用?

    本文作者为霍格沃兹测试学院第 9 期学员 zzt

    业务背景

    我们是一家手游公司,前端使用 Unity。Appium 之类框架的都无法识别 Unity 控件,最后得知网易Airtest 下面的 poco 框架可识别 Unity 控件。

    由于之前没有相关经验靠自己摸爬滚打,走了很多弯路,代码结构/框架也重构了几次(现在还想重构:joy: )。在设计之初有过很多构想,觉得应该满足那些要求:

    颗粒度尽可能小且case互不影响

    可根据不同策略执行不同深度case集

    负载均衡:收集可用测试机根据对应测试机执行快慢分发不同数量case任务

    重复执行

    失败重试 (因业务特殊目前不准备失败重试,因为case前置数据准备是通过跑sql修改数据,但前端不会及时刷新,需要找到一个刷新点,主流的刷新点是重登,从当前case界面-》跳转游戏主界面-》设置切换账号-》登录界面登录账号-》跳转游戏主界面-》跳转case指定界面,这个过程非常耗时,会大幅增加case执行时间)

    让case编写者只需要关注业务

    以最小的改动面对未来需求的变化

    ......

    还实现了很多,这里不一一列举.

    问题来了

    了解到 Page Object 模式很主流,很火。然后使用 yaml 数据驱动,很炫酷,高大上的样子(想立马就应用到项目中)。

    但 UI 自动化测试到底要不要用 Page Object 模式,以及 yaml 数据驱动?或者说我这个情况要不要使用 PO 模式?

    任何技术最终还要是服务于业务,是必须要能解决某些或某类问题的。这里以我对 PO 模式非常浅显的理解和我当前的做法做了个对比:

    Page Object 模式很火,UI 自动化测试到底要不要用?怎么用?

    单看表格可能看不懂哈,直接贴 Python 代码(省去了case前置界面准备,前置数据准备):

    代码内容

    代码主要是把一个战术技能从0级升级到10, 并做相关断言。

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> data_0 = [

    ['0', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+0</color>', '1', '15001', 9],

    ['1', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+1</color>', '2', '15002', 9],

    ['2', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+2</color>', '4', '15003', 9],

    ['3', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+4</color>', '6', '15004', 9],

    ['4', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+6</color>', '9', '15005', 9],

    ['5', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+9</color>', '12', '15006', 9],

    ['6', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+12</color>', '16', '15007', 9],

    ['7', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+16</color>', '20', '15008', 9],

    ['8', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+20</color>', '25', '15009', 9],

    ['9', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+25</color>', '30', '15010', 9],

    ['10', '每有一个进攻战术达到<color=#fca926>10级</color>,\n球队进攻<color=#fca926>+30</color>', '0', '0', 0],

    ]

    @allure.story('战术素养')

    @allure.title('升级常规进攻素养0-10级')

    @pytest.mark.parametrize('level, des, number, style, t_book', data_0)

    def test_0(self, mt, level, des, number, style, t_book):

    poco = mt.poco

    poco("Content").child("TacticsStyleItem(Clone)")[0].click() #选中常规进攻素养

    assert poco("Content").child("TacticsStyleItem(Clone)")[0].child('GiftType').get_text().split('.')[1] == level

    检查当前进攻素养的等级是否正确

    assert poco("DetailPanel").child("CurrentEffect").child('Desc').get_text() == des #断言当前进攻素养的文案内容是否正确

    if level != '10':

    poco("UpgradePanel").child("UpgradeBtn").click() #点击升级按钮

    poco("Content").child("TacticsStyleItem(Clone)")[0].click() #点击跳过动画

    assert mt.sql.select(SQL_1_0+style)[1][0] == t_book, SQL_1_0+style #查看数据库对应类型道具应该减少一个

    assert poco("CurrentEffect").child('Desc').get_text().split('+')[1].split('<')[0] == number #升级后加成的数值是否正确

    else:

    assert poco("UpgradePanel").child("UpgradeBtn").child("Label").get_text() == "已满级" #满级时应不能升级</pre>

    疑惑

    这样看来,PO 模式会更加繁琐,笨重,好像 PO 模式没有什么优势。

    深入探索 PO 模式

    经过在 TesterHome 社区发帖讨论,参考了大家的很多观点,有倾向于 PO 模式,也有建议根据不同项目场景自行处理,感觉还是有些一知半解,于是集中深入的了解了下 Page Object 模式。

    Page Object模式 Python WebDriver 版本

    这里介绍下我近期对 PO 模式的理解,整体思想是:分层,让不同层去做不同类型的事情,让代码结构清晰,增加复用性

    一般分两层或三层(也有四层的):

    两层:对象逻辑层+业务数据层。

    三层:对象库层+逻辑层+业务层。

    四层:对象库层+逻辑层+业务层+数据层。

    不同分层本质差不多。

    下面以登录为例子(网上绝大多数都是以登录为例子,但登录只能让新手明白 PO 大概是怎样子,优势却很难传递出来)。

    普通方式如下:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">def test_user_login():

    driver = webdriver.Edge()

    base_url = 'https://mail.qq.com/'

    username = '3494xxxxx' # qq号码

    password = 'kemixxxx' # qq密码

    driver.get(base_url)

    driver.switch_to.frame('login_frame') #切换到登录窗口的iframe

    driver.find_element(By.ID, "u").send_keys(username) #输入账号

    driver.find_element(By.ID, "p").send_keys(password) #输入密码

    driver.find_element(By.ID, "login_button").click() #点击登录</pre>

    PO 模式实现

    对象库层

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">#创建基础类

    class BasePage(object):

    初始化

    def init(self, driver):

    self.base_url = 'https://mail.qq.com/'

    self.driver = driver

    self.timeout = 30

    打开页面

    def _open(self):

    url = self.base_url

    self.driver.get(url)

    self.driver.switch_to.frame('login_frame') #切换到登录窗口的iframe

    def open(self):

    self._open()

    定位方法封装

    def find_element(self,*loc):

    return self.driver.find_element(*loc)</pre>

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">#创建LoginPage类

    class LoginPage(BasePage):

    username_loc = (By.ID, "u")

    password_loc = (By.ID, "p")

    login_loc = (By.ID, "login_button")

    输入用户名

    def type_username(self,username):

    self.find_element(*self.username_loc).send_keys(username)

    输入密码

    def type_password(self,password):

    self.find_element(*self.password_loc).send_keys(password)

    点击登录

    def type_login(self):

    self.find_element(*self.login_loc).click()</pre>

    逻辑层

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">#创建test_user_login()函数

    def user_login(driver, username, password):

    """测试用户名/密码是否可以登录"""

    login_page = LoginPage(driver)

    login_page.open()

    login_page.type_username(username)

    login_page.type_password(password)

    login_page.type_login()</pre>

    业务层

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">def test_user_login():

    driver = webdriver.Edge()

    username = '3494xxxxx' #qq号码

    password = 'kemixxxx' #qq密码

    test_user_login(driver, username, password)</pre>

    分析对比 PO 优劣势

    一、代码量多了大概三倍

    代码量增加是一定的,这里先忽略,后面重点讨论。

    二、分层之后真的易于维护吗?

    我们来看下当元素发生变化的时候,只需要在对象库层找打对应元素修改。咦?你会说普通方式不也一样吗?看上去一样,其实有细微差异,而一些细微差异会导致很大不同:

    • 效率高:PO 模式每个元素有变量定义,更方便查找。而普通方式得通过备注或上下文来推断效率低。

    p.s. 随着 case 不断增加,海量元素的定义对于英语一般的同学挑战也大,有人说有谷歌翻译。定义的时候可以通过翻译,但到时候回过来查过元素怎么办?翻译通常是1对多,我们当时选哪个?用哪个来搜索?这或许也是海量变量定义带来的困扰。

    • 复用多收益大:当某个元素被多次引用的时候,只需要修改一处便可,而普通方式需要一处一处找出来并修改,可以看出来复用越多 PO 模式收益越大。

    当界面需求发生变化:

    1. 新增或删除了一些功能点或调整操作步骤先后顺序,但上层业务不变。

    • 效率高 :同理,PO模式的逻辑层方法有具体定义,情况和元素发生变化一样。修改逻辑层,业务层不变。这样看来结构简单清晰,舒服更符合人类习惯,普通方式就是继续堆 case。
    • 复用多收益大:同样这里如果逻辑复用越多,PO 模式收益越大,因为对于 PO 模式来说都只需要修改一个地方多处受益.

    2. 上层业务发生变化,看上去两者差异不大。

    所以整体来看:

    • case 越多使用 PO 模式会使你的代码结构更清晰
    • 元素复用越多 PO 模式下维护非常容易
    • 逻辑复用越多 PO 模式下维护非常容易(如果逻辑复用多,需要多考虑逻辑层的颗粒度)
    • 元素/逻辑/数据复用越多应选择更多层的 PO 模式
    Page Object 模式很火,UI 自动化测试到底要不要用?怎么用?

    好,我们再回过头来看看代码量大的问题,有没有办法精简一些呢? 把 a*N 中的a变成1.8,1.5, 1.2,甚至接近 1 呢?开始下一轮探索。

    探索代码量大的问题

    以三层 PO 为例我们大概的流程是这样的:

    在对象库层,我们定义了元素,再为元素定义了一些基本的操作流,在逻辑层集成了基本操作流,在业务层组装逻辑和数据输入。

    看上去第二、第三步骤有点重复,能不能去掉?如果只剩下第一、四个步骤,那代码量瞬间就下来了。那该有多爽。

    试试看

    如果去掉第二/三步骤,那意味着我们只需要定义元素,并在业务层需要指定操作的时候再自动生成对应所需操作。即需要时生成,用完后丢弃。

    这里需要用到 Python 下面的魔法方法 "getattribute"

    思路:

    在访问类 App 属性时挡截下来,历遍对象库层找到对应元素返回对应的对象类 App.LoginPage,而对象库层都继承了BasePage 类,在 BasePage 中同样重构了"getattribute",当 App.LoginPage 对象尝试调用 click() 之类的方法时,就临时绑定 click 方法(click/swip/gettext/settext.....)。

    Page Object 模式很火,UI 自动化测试到底要不要用?怎么用?

    这样做的话,就只需要编写元素对象库,在 业务层直接自由调用,即时生成,用完丢弃。代码量大幅减少。

    对象库层

    这里使用 Airtest 下面的 poco 控件识别框架举例,和 Appium Selenium 略微不同。

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class AndroidHomePage(BasePage):

    def init(self, driver):

    super().init(driver)

    self.p_account= "NormalWindow/AccountInputField"

    self.p_password = "NormalWindow/PwdInputField"</pre>

    业务层

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">def test_login(id,pw)

    App.LoginPage.p_account.set_text(id)

    App.LoginPage.p_password.set_text(pw)</pre>

    如此看来用例编写者就更接近只需要关注业务

    以下是关键思路的实现:

    以 App 类为例子,BasePage Element 大概相同。

    App 类

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">def getattribute(self, attr):

    """

    挡截属性访问

    """

    target_page = None

    if attr.endswith('page'): # 过滤page

    page = import_module(attr) #历遍 对象库层目录src/page 找到目标文件

    if self.client_version == CHINA_PLATFORM: # 国内版本

    for item in page.dict:

    if item.startswith(CHINA_PAGE_PREFIX) :

    target_page = getattr(page, item)

    elif self.client_version == OVERSEAS_PLATFORM: # 海外版本

    for item in page.dict:

    if item.startswith(OVERSEAS_PAGE_PREFIX) :

    target_page = getattr(page, item)

    return target_page(self._driver)

    else:

    非过滤直接访问

    return object.getattribute(self, attr)</pre>

    添加部分具体代码

    对象元素库层

    BasePage:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class BasePage(object):

    def init(self, driver):

    self.poco = driver

    self._p_help_btn = ['帮助按钮', 'HelpBtn']

    “帮助按钮" 为注释,有几个作用:

    1.用于之后元素查找

    2.访问self._p_help_btn 时自动绑定一个_name属性并赋值,在使用click()等具体操作时,会自动给对应方法添加 with allure.step

    (“步骤:点击 %s”% self._name) (allure报告框架)从而使每个case都会展示具体的操作步骤信息;“HelpBtn” :

    具体的元素(这里是以poco框架的元素为例)

    self._p_help_text = ['帮助文本信息', 'GuideDialog(Clone)/DialogTx']

    self._p_help_continue_btn = ['帮助-》继续按钮', 'GuideDialog(Clone)/Continute/Glim']

    self._dict = object.getattribute(self, 'dict') # 获取属性集用于历遍查找目标属性

    解析key的方法,不同提取元素框架自行实现解析函数,返回一个对应框架的控件操作对象

    def resolve_poco(self, key):

    return poco_key(self.poco, key)

    def getattribute(self, attr):

    挡截 “p_” 和“p”的属性,“p”通常为BasePage的通用元素,加下横线用以区分

    if attr.startswith('p_') or attr.startswith('p'):

    _proxy = self.resolve_poco(self._dict[attr][1]) #获取对应元素操作对象的代理

    _proxy._name = self._dict[attr][0] #绑定注释信息

    _proxy.click = types.MethodType(allure_click, _proxy) #绑定click方法

    return _proxy

    else:

    return object.getattribute(self, attr)

    这里的帮助文档检查是每个功能模块都有的,所有放在BasePage里面,不同继承类如果有元素差异重写元素即可

    def check_help_text(self, texts, timeout=3):

    self._p_help_btn.click()

    for text in texts:

    self.regular_wait(timeout)

    assert self._p_help_text.wait(2).get_text() == text

    self._p_help_continue_btn.click()</pre>

    ScoutingPage:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">class ScoutingPage(BasePage):

    def init(self, driver):

    super().init(driver)</pre>

    业务层:

    <pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">@allure.story('帮助')

    @allure.title('文案检查')

    def test_0(self, mt):

    mt 是pytest下面的一个fixture,完成了一系列操作最后返回对应的Page类对象,操作包括:登录/前置界面智能跳转/前置数据准备等等

    mt.check_help_text([

    '球探介绍所可以帮助球队搜索到潜力新星,但每次搜索需要消耗大量机票',

    '董事会每<color=#FFBE34>5</color>分钟会赞助球队1张机票,解雇球员也可以获得大量机票'

    ])</pre>

    执行报告如下:

    Page Object 模式很火,UI 自动化测试到底要不要用?怎么用?

    以上,是对 PO 的一点探索和新认识,欢迎大家多多指点。(end)

    (文章来源于霍格沃兹测试学院)

    相关文章

      网友评论

          本文标题:Page Object 模式很火,UI 自动化测试到底要不要用?

          本文链接:https://www.haomeiwen.com/subject/mjjzmhtx.html