基于Poco框架的面向页面UI自动化测试设计

1 篇文章 0 订阅
1 篇文章 0 订阅

Poco框架上手容易,与Airtest无缝结合,能快速解决一些棘手的页面校验问题,因此受到笔者的喜爱。Poco框架下测试代码是这样的(官方示例):
将星星拖拽到贝壳

# coding=utf-8

import time
from poco.drivers.unity3d import UnityPoco

poco = UnityPoco()

poco('btn_start').click()  # 根据name定位元素
poco(text='drag drop').click()  # 根据text属性定位元素
time.sleep(1.5)

shell = poco('shell').focus('center')  # 得到shell元素的一个copy
for star in poco('star'):  # 遍历star元素  
    star.drag_to(shell)  # 依次放入shell中
time.sleep(1)

assert poco('scoreVal').get_text() == "100", "score correct."  # 校验游戏得分
poco('btn_back', type='Button').click()  # 通过name, type属性组合定位元素

这样的代码执行起来当然没有问题,但是元素、方法都没有经过封装,一旦出现变更,改起来还是挺累,这时便需要考虑Page-Objects了。
我们期望看到的Page代码可能是这样的:

# coding=utf-8

import time
from poco.drivers.unity3d import UnityPoco

poco = UnityPoco()


class GamePage:
    btn_start = poco('btn_start')
    drag_drop = poco(text='drag drop')
    shell = poco('shell').focus('center')
    stars = poco('star')
    scoreVal = poco('scoreVal')

    def drag_star_to_shell(self):
        for star in self.stars:  # 遍历star元素
            star.drag_to(self.shell)  # 依次放入shell中
            time.sleep(1)

    def get_score(self):
        return self.scoreVal.get_text()

#  下面是测试用例:
def test_drag_star_to_shell():
    game = GamePage()
    game.drag_star_to_shell()
    score = game.get_score()
    assert score==100, '期望得分100,实际得分为:{}'.format(score)

如果只是演示,上面的代码执行起来也没太大问题,但放到实际项目中,就完全不可行了。
问题出在元素定位环节,每当实例化一个GamePage对象,类似’btn_start = poco(‘btn_start’)'这样的代码都会导致poco根据设置的定位条件查找元素,一旦有任何元素找不到,就会出错中止测试。即使都能找到,我们肯定也不期望每次都去找一遍,用例执行效率太低。
下面是解决方案:

  • 首先通过airtest录制得到测试元素,可以是poco(‘button’)、poco(text=‘hello’)这种形式,也可以是poco(“android.widget.LinearLayout”).offspring(“android:id/content”).offspring(“net.csdn.csdnplus:id/fl_container”).offspring(“net.csdn.csdnplus:id/ll_order_tag”).offspring(“net.csdn.csdnplus:id/slide_tab”).child(“android.widget.LinearLayout”).child(“android.widget.RelativeLayout”)[4].child(“net.csdn.csdnplus:id/tv_tab_title”)这样的UI
    path-code。
  • 在Airtest中打印元素query,比如,执行print(poco(text=‘Python’).query),结果如下图:
    打印元素query

红框部分的“(‘and’, ((‘attr=’, (‘text’, ‘Python’)),))”就是我们需要的内容,Poco使用元组来构造元素的定位字符串,如果大家对其中逻辑有兴趣可以自行翻看源码,这里不多做解释。

  • 拿到定位字符串后,我们的Page页面就可以实现,测试代码可以改写成如下形式:
# coding=utf-8

import time
from poco.drivers.unity3d import UnityPoco
from poco.proxy import UIObjectProxy  # 添加引用,用于构造测试元素
poco = UnityPoco()

class GamePage:
    btn_start_locator = ('and', (('attr=', ('name', 'btn_start')),))  # 这里使用元组,而不是字符串
    drag_drop_locator = ('and', (('attr=', ('text', 'drag_drop ')),))
    shell_locator = ('and', (('attr=', ('name', 'shell')),))
    stars_locator = ('and', (('attr=', ('name', 'stars')),))
    scoreVal _locator = ('and', (('attr=', ('name', 'scoreVal ')),))
    
    def drag_star_to_shell(self):
        stars = UIObjectProxy(poco) #  固定格式
        stars.query = self.stars_locator  # 按需构造使用的元素
        shell_temp = UIObjectProxy(poco)
        shell_temp.query = self.shell_locator
        shell = shell_temp.focus('center')
        for star in stars:  # 遍历star元素
            star.drag_to(shell)  # 依次放入shell中
            time.sleep(1)

    def get_score(self):
        scoreVal = UIObjectProxy(poco)
        scoreVal.query = self.scoreVal _locator
        return scoreVal.get_text()

通过上面的方案就实现了一般场景下的面向页面设计,但是易用性有点问题,构造元素造成代码行数较多,所以我们可以写一个元素初始化的方法:

def init_element(locator: tuple) -> UIObjectProxy: # 接受元组作为参数,返回一个UIObjectProxy对象
    element = UIObjectProxy(poco)
    element.query = locator
    return element

在此方法的基础上,我们的页面可以优化为:

# coding=utf-8

import time
from poco.drivers.unity3d import UnityPoco
from poco.proxy import UIObjectProxy  # 添加引用,用于构造测试元素
poco = UnityPoco()
def init_element(locator: tuple) -> UIObjectProxy: # 接受元组作为参数,返回一个UIObjectProxy对象
    element = UIObjectProxy(poco)
    element.query = locator
    return element
class GamePage:
    btn_start_locator = ('and', (('attr=', ('name', 'btn_start')),))  # 这里使用元组,而不是字符串
    drag_drop_locator = ('and', (('attr=', ('text', 'drag_drop ')),))
    shell_locator = ('and', (('attr=', ('name', 'shell')),))
    stars_locator = ('and', (('attr=', ('name', 'stars')),))
    scoreVal _locator = ('and', (('attr=', ('name', 'scoreVal ')),))
    
    def drag_star_to_shell(self):
        stars = init_element(self.stars_locator )
        shell_temp= init_element(self.shell_locator )
        shell = shell_temp.focus('center')
        for star in stars:  # 遍历star元素
            star.drag_to(shell)  # 依次放入shell中
            time.sleep(1)

    def get_score(self):
        scoreVal = init_element(self.scoreVal _locator)
        return scoreVal.get_text()

这样代码量及习惯跟一般的面向页面就比较类似,还剩一个问题需要解决,元素的query字符串在实际项目中有可能会很复杂,特别是当使用UI path-code时,比如我想点页面中的第二篇文章:
在这里插入图片描述
Airtest录制出来的UI path-code就很复杂:
poco(“android.widget.LinearLayout”).offspring(“android:id/content”).offspring(“net.csdn.csdnplus:id/fl_container”).child(“android.widget.RelativeLayout”).offspring(“net.csdn.csdnplus:id/refreshLayout”).offspring(“net.csdn.csdnplus:id/recyclerView”).child(“android.widget.LinearLayout”)[2].offspring(“net.csdn.csdnplus:id/tv_title”)

再获取query字符串

('>', (('index', (('/', (('>', (('>', (('/', (('>', (('>', (('and', (('attr=', ('name', 'android.widget.LinearLayout')),)), ('and', (('attr=', ('name', 'android:id/content')),)))), ('and', (('attr=', ('name', 'net.csdn.csdnplus:id/fl_container')),)))), ('and', (('attr=', ('name', 'android.widget.RelativeLayout')),)))), ('and', (('attr=', ('name', 'net.csdn.csdnplus:id/refreshLayout')),)))), ('and', (('attr=', ('name', 'net.csdn.csdnplus:id/recyclerView')),)))), ('and', (('attr=', ('name', 'android.widget.LinearLayout')),)))), 2)), ('and', (('attr=', ('name', 'net.csdn.csdnplus:id/tv_title')),))))

这么多的字符,看起来还是挺难受的。
这个问题笔者也没有很好的解决方案,采取借助自研测试平台的方法来迂回解决,没有测试平台也可以导入到数据库。
整体思路:

  1. 在测试平台中管理测试页面,包括元素以及方法
  2. 测试元素通过python脚本自动将query字符串导入到平台
  3. 在测试平台中根据已有的元素,将通用业务操作封装成页面方法
  4. 导出页面对象到测试框架
  5. 结合业务需求,利用页面对象完成测试脚本的设计

导入元素的核心代码如下:
备注:这里有个小技巧,笔者是修改了poco.proxy文件,为元素添加了do_import方法,这样在Airtest中录制元素后,将代码中.click()替换为.do_import()就可以实现上传。

def do_import(self, host, project_name, page_name, element_desc): 
        """
        host, project_name, page_name约定了将元素上传到指定项目的指定页面下
        element_desc是元素的中文名称,调用百度翻译接口,加上一些简单的字符串处理自动生成英文名
        """
        locator = self.query
        locator_str = str(locator)  #  转成字符串
        page_id = get_page_id(host, project_name, page_name) #  获取页面ID
        name = fy(element_desc)  # 由翻译获取
        search_url = host + '/search_element2/'
        if check_element(search_url, page_id, name) == 0:
            create_url = host + '/create_element/'
            try:
                create_data = {"name": name, "page_id": page_id, "locator": locator_str,
                               "remark": element_desc}
                create_result = requests.post(create_url, json=create_data)  # 发起post导入测试元素
                print('{}页面已成功导入{}元素'.format(page_name, element_desc))
                return True
            except:
                print('导入失败,请检查网络及数据库连接')
        else:
            print('{}页面中已存在名称为{}的同名元素,放弃导入'.format(page_name, name))
            abort_element_file = 'd:\\' + page_name + '_aborted_elements.txt'
            with open(abort_element_file, 'a', encoding='utf-8') as f:
                time_now = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))
                f.write('时间:{},element name:{},element 定位方式:{},element备注:{}\n'.format(time_now, name, locator,
                                                                                      element_desc))
            return False

在Airtest中上传元素的代码:

project_name = 'xx小程序_temp'
page_name = 'order_list_page'
host = 'http://172.18.0.184:8002'
desc = '搜索栏'
poco("net.csdn.csdnplus:id/tv_search_content").do_import(host,project_name,page_name,desc)

最后,其实我们还可以写一个批量自动导入,只要当前UI中的元素节点存在text或者desc属性,就将其作为测试元素自动上传。

  • 这个方法只适用于页面上的元素大部分是测试需要的场景。
  • 逆向思维,如果这个元素没有text属性,一般情况下测试人员也很难知道它到底该叫什么,就不要自动导入了。
def auto_import(poco, _host, _project_name, _page_name):
    ui = poco.agent.hierarchy.dump()  # 导出UI树
    temp_list = jsonpath.jsonpath(ui, '$..text')  # 利用jsonpath找到text节点
    success = 0
    failed = 0
    for _element_remark in temp_list:
        if re.search(u'^[_a-zA-Z0-9\u4e00-\u9fa5]+$', _element_remark):  # 正则过滤掉一些不正经的元素
            element_temp = UIObjectProxy(poco)
            element_temp.query = "('and', (('attr=', ('text', '" + _element_remark + "')),))"  # 根据text属性构造元素
            if element_temp.do_import(_host, _project_name, _page_name, _element_remark):  # 循环调用上文的do_import方法
                success += 1
            else:
                failed += 1
    print('{}页面{}个元素导入成功,{}个元素导入失败,失败详情请查看D盘根目录下的{}_aborted_elemnt.txt文件'.format(_page_name, success, failed, _page_name))

我们的测试元素处理流程:

  1. 手机连接Airtest;
  2. 进入测试页面,;
  3. 设置好项目名称,页面名称等下信息;
  4. auto_import();
  5. 人为查缺补漏

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值