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

MarvinWu · January 14, 2020 · Last by 小怪兽 replied at February 26, 2020 · 1413 hits

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就会简洁很多

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up