Airtest 基于 Poco 框架的面向页面解决方案

MarvinWu · 2020年01月14日 · 最后由 小怪兽 回复于 2020年02月26日 · 351 次阅读

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 根据设置的定位条件查找元素,一旦有任何元素找不到,就会出错中止测试。即使都能找到,我们肯定也不期望每次都去找一遍,用例执行效率太低。
下面是解决方案:
1.首先通过 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。
2.在 Airtest 中执行 print(element.query),如下图:

3.红框部分的 “('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")[3].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')),)))), 3)), ('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. 人为查缺补漏

共收到 4 条回复 时间 点赞

从文中看出,作者的出发点一是解决偶尔定位不到的问题

第二是解决定位的效率

还有其它的出发点吗

另外,有统计过效率大概提升多少了吗,比如原来跑一遍用例 10 小时,现在能降低几个小时

如果收益不大,是否有足够的必要做这么大的工作?!

游戏还是没有控件,如果搜索控件 id 的话,感觉不如 无障碍功能好用了。

秦岭 回复

看楼主这个 好像并不是减少用例执行时间(case 编写完毕后)。 而是减少或杜绝编写 case 时元素错误问题


楼主这个是想自动收集所有元素吗? 好像没看到命名环节
如果是手动,那定位元素不要使用 airtestIDE 默认给的,在左侧树状图里自己手动 copy 就会简洁很多

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册