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 属性,就将其作为测试元素自动上传。

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. 人为查缺补漏


↙↙↙阅读原文可查看相关链接,并与作者交流