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')),))))
这么多的字符,看起来还是挺难受的。
这个问题笔者也没有很好的解决方案,采取借助自研测试平台的方法来迂回解决,没有测试平台也可以导入到数据库。
整体思路:
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))
我们的测试元素处理流程: