Airtest自动化测试第三篇:Poco控件识别——精准定位UI元素的进阶武器

📅 2026/7/9 3:06:21
Airtest自动化测试第三篇:Poco控件识别——精准定位UI元素的进阶武器
上一篇我们学会了图像识别脚本编写本篇进入Airtest的第二大核心能力——Poco控件识别。如果说图像识别是用眼睛看那Poco就是用探针刺入APP内部直接读取UI树结构稳定性和效率远超图像识别。一、Poco是什么1.1 一句话理解Poco是Airtest的控件识别引擎通过读取APP的UI控件树直接用控件名控件属性来定位和操作元素而不是依赖截图匹配。图像识别Airtest看屏幕 → 找图 → 点击坐标 控件识别Poco 读UI树 → 找控件 → 调用控件方法1.2 为什么要用Poco场景图像识别Poco控件识别按钮换了颜色❌ 需要重新截图✅ 不影响控件名没变列表项数量不确定❌ 很难处理✅ 直接遍历控件列表获取文本内容❌ 做不到✅get_text()直接取动态加载的元素❌ 不可靠✅wait_for_appearance()跨分辨率运行❌ 需要多套截图✅ 完全不受影响速度较慢截图匹配快直接访问UI树1.3 Poco的适用条件Poco不是万能的使用有前提✅ Android原生APP大部分支持✅ iOS原生APP需要注入Poco-SDK✅ Unity3D游戏集成Poco-SDK✅ Cocos2d游戏集成Poco-SDK✅ 微信小程序需开启调试模式❌ 纯H5页面需要用WebView模式❌ 部分加固APP无法获取UI树二、Poco与Airtest的关系2.1 双引擎架构再回顾python# 一个脚本中同时使用两种引擎 from airtest.core.api import * # Airtest 图像识别引擎 from poco.drivers.android.uiautomation import AndroidUiautomationPoco # 初始化Poco poco AndroidUiautomationPoco() # 图像识别点击设置图标 touch(Template(rtpl1678888888.png, record_pos(0.2, -0.8), resolution(1080, 2340))) # 控件识别点击文本为设置的按钮 poco(text设置).click()2.2 何时用Airtest何时用Poco情况推荐方案控件能被Poco识别到优先Poco更稳定控件获取不到自定义绘制/游戏画面用Airtest图像识别操作后需要等待页面变化Pocowait_for_appearance()需要断言画面效果颜色/位置Airtestassert_exists()获取元素文本/属性值Pocoattr()/get_text()滑动操作Airtestswipe()更通用启动/关闭APPAirteststart_app()/stop_app()断言 报告Airtest 断言API核心原则能用Poco就用PocoPoco搞不定再用图像识别兜底。三、Poco辅助窗——你的控件侦察机3.1 打开Poco辅助窗在Airtest IDE中连接设备后点击左侧工具栏的Poco辅助窗按钮选择对应模式Android/iOS/Unity等等待UI树加载完成3.2 辅助窗能做什么关键操作点击UI树中的控件 → 手机屏幕上对应元素高亮点击手机屏幕上的元素 → UI树中自动定位右键控件 → 复制定位代码 → 粘贴到脚本中3.3 三种获取定位代码的方式方式一右键复制在UI树上右键控件 → 复制 → 选择复制格式poco(text登录)—— 基础选择器poco(android.widget.Button)[0]—— 类型索引方式二录制模式点击录制按钮 → 在手机上操作 → 自动生成Poco代码方式三手动编写推荐熟练掌握 根据属性面板的信息自己写定位代码四、Poco选择器——三种定位方式详解4.1 基本选择器python复制# 语法poco(属性名属性值) # 1. 按节点类型定位 poco(Button) # 2. 按text属性定位最常用 poco(text登录) # 3. 按name属性定位 poco(nameandroid.widget.Button) # 4. 按其他属性定位 poco(desc返回按钮) poco(checkedTrue) poco(focusedFalse) # 5. 多属性组合AND关系 poco(text登录, enabledTrue) poco(android.widget.Button, text确认)4.2 相对选择器层级定位通过父子、兄弟关系定位控件python# 1. 子节点定位先找到父节点再找子节点 poco(父节点).child(子节点) poco(ScrollView).child(Button) # ScrollView下的Button # 2. 后代节点定位孙子也行 poco(父节点).offspring(后代节点) poco(MainPage).offspring(text搜索) # MainPage下任意层级的搜索按钮 # 3. 兄弟节点定位 poco(目标控件).sibling() # 所有兄弟节点 poco(目标控件).sibling()[0] # 第一个兄弟节点 # 4. 父节点定位 poco(text登录).parent() # 登录按钮的父节点 # 5. 链式调用组合定位 poco(FrameLayout).child(LinearLayout).child(text登录)4.3 正则表达式选择器这是处理动态文本和模糊匹配的利器pythonimport re # 1. 匹配以订单开头的文本 poco(textMatches^订单.*) # 2. 匹配包含2025的文本 poco(textMatches.*2025.*) # 3. 匹配以数字结尾的文本 poco(textMatches.*\d$) # 4. 使用re.compile推荐可加标志 poco(textMatchesre.compile(r^确认|确定$)) # 匹配确认或确定 poco(textMatchesre.compile(r第\d条, re.IGNORECASE)) # 5. 实战案例匹配各种提示弹窗 poco(textMatches.*成功.*).wait_for_appearance() poco(textMatches.*(失败|错误|异常).*)注意textMatches是Airtest 1.2版本新增的属性使用正则表达式时必须用它而不是text。4.4 选择器进阶技巧python# 1. 获取选择器匹配到的所有控件 all_buttons poco(Button) # 返回所有Button print(len(all_buttons)) # 查看数量 # 2. 索引选择第几个 poco(Button)[0].click() # 第一个Button poco(Button)[-1].click() # 最后一个Button # 3. 遍历控件列表 for item in poco(android.widget.TextView): print(item.get_text()) # 4. 判断控件是否存在不报错 if poco(text登录).exists(): poco(text登录).click() # 5. 复合条件类型 属性 poco(android.widget.Button, text登录) # Button类型且text为登录五、Poco核心API——操作与查询5.1 操作类APIpython# 1. 点击 poco(text登录).click() poco(text登录).click(pos[0.5, 0.5]) # 点击控件中心默认 poco(text登录).click(pos[0.1, 0.5]) # 点击控件左侧 poco(text登录).long_click() # 长按 poco(text登录).long_click(duration3.0) # 长按3秒 poco(text登录).double_click() # 双击 # 2. 滑动 poco(ScrollView).swipe([0.5, 0.8], [0.5, 0.2]) # 从下往上滑 poco(ScrollView).swipe([0.5, 0.2], [0.5, 0.8]) # 从上往下滑 poco(ScrollView).swipe([0.8, 0.5], [0.2, 0.5]) # 从左往右滑 # 拖拽 poco(可拖拽控件).drag_to(poco(目标位置)) # 3. 文本输入 poco(EditText).set_text(hello) # 设置文本替换 poco(EditText).set_text() # 清空 # 4. 获取控件信息 poco(text登录).get_text() # 获取文本 poco(text登录).get_name() # 获取控件类型名 poco(text登录).get_position() # 获取位置 [x, y] poco(text登录).get_size() # 获取大小 [w, h] poco(text登录).get_bounds() # 获取边界 [x, y, w, h] # 5. 判断控件状态 poco(text登录).exists() # 是否存在True/False5.2 属性读取APIpython# attr() —— 读取控件的任意属性 button poco(text登录) # 常用属性读取 button.attr(text) # 登录 button.attr(name) # android.widget.Button button.attr(enabled) # True / False button.attr(visible) # True / False button.attr(clickable) # True / False button.attr(checked) # True / False button.attr(focused) # True / False button.attr(desc) # 无障碍描述 button.attr(pos) # [0.5, 0.8] button.attr(size) # [0.4, 0.06] # 遍历控件所有属性 for attr_name in button.attr(allAttributes): print(f{attr_name} {button.attr(attr_name)}) # setattr() —— 修改控件属性慎用 poco(text开关).setattr(checked, True) # 设置开关状态5.3 等待APIpython# 1. wait_for_appearance() —— 等待控件出现最常用 poco(text加载完成).wait_for_appearance(timeout30) # 超时30秒不报错 poco(text加载完成).wait_for_appearance() # 默认120秒 # 2. wait_for_disappearance() —— 等待控件消失 poco(text加载中...).wait_for_disappearance(timeout60) # 3. wait(timeout) —— 直接等待控件出现超时则报错 poco(text登录成功).wait(10) # 10秒内必须出现 # 4. Poco类的批量等待 # wait_for_all() —— 所有控件都出现 poco.wait_for_all([poco(text标题), poco(text登录), poco(text注册)]) # wait_for_any() —— 任意一个出现即可 result poco.wait_for_any([poco(text成功), poco(text失败)])5.4 判断与断言python# 1. exists() —— 判断是否存在不报错不写报告 if poco(text弹窗).exists(): poco(text关闭).click() # 2. 配合Airtest断言写入报告 poco(text支付成功).wait_for_appearance() assert_exists(Template(r支付成功图标.png, 验证支付成功页面)) # 3. 文本断言 assert poco(text订单号).get_text() ABC123456 # 4. 属性断言 assert poco(text开关).attr(checked) True六、实战案例集6.1 案例一登录流程Poco版pythonfrom airtest.core.api import * from poco.drivers.android.uiautomation import AndroidUiautomationPoco poco AndroidUiautomationPoco() # 1. 等待登录页加载 poco(text登录).wait_for_appearance(timeout10) # 2. 输入账号密码 poco(android.widget.EditText)[0].set_text(admin) poco(android.widget.EditText)[1].set_text(123456) # 3. 点击登录 poco(text登录).click() # 4. 验证登录结果 if poco(text登录成功).wait_for_appearance(timeout5): print(登录成功) poco(text确定).click() elif poco(text登录失败).exists(): print(登录失败) # 截图保存失败现场 snapshot(msg登录失败截图)6.2 案例二列表遍历与验证python# 场景验证商品列表每个商品都显示了价格 poco(商品列表页).wait_for_appearance() # 获取所有商品项 items poco(com.example.app:id/item_product) print(f共找到 {len(items)} 个商品) # 遍历验证 for i, item in enumerate(items): # 获取商品名称 name item.child(com.example.app:id/tv_name).get_text() # 获取价格 price_text item.child(com.example.app:id/tv_price).get_text() # 验证价格不为空 if price_text and ¥ in price_text: print(f商品{i1}: {name} - 价格 {price_text} ✓) else: print(f商品{i1}: {name} - 价格异常 ✗) snapshot(msgf商品{i1}价格异常)6.3 案例三下拉刷新与加载更多python# 场景下拉刷新列表验证加载更多 # 1. 获取列表控件 list_view poco(android.widget.ListView) # 2. 下拉刷新 list_view.swipe([0.5, 0.5], [0.5, 0.8]) # 从中间往下滑 # 3. 等待刷新完成 poco(text刷新中...).wait_for_disappearance(timeout10) sleep(1) # 4. 上滑加载更多 list_view.swipe([0.5, 0.8], [0.5, 0.2]) # 从下往上滑 # 5. 等待加载完成 poco(text加载中...).wait_for_disappearance(timeout10) # 6. 验证新内容出现 assert poco(text加载完成).exists() or poco(com.example.app:id/item_product)[-1].exists()6.4 案例四处理随机弹窗python# 场景首页可能有活动弹窗需要先关闭 def handle_popup(): 通用弹窗处理函数 # 匹配各种可能的弹窗关闭按钮 close_patterns [ poco(text关闭), poco(text×), poco(text取消), poco(desc关闭), poco(textMatches.*跳过.*), poco(textMatches.*知道了.*), ] for close_btn in close_patterns: if close_btn.exists(): close_btn.click() sleep(0.5) return True return False # 使用 poco(text首页).wait_for_appearance(timeout10) sleep(1) # 等待弹窗可能出现 # 最多尝试3次关闭弹窗 for i in range(3): if handle_popup(): print(f第{i1}次关闭了弹窗) sleep(1) else: break6.5 案例五动态内容正则匹配python# 场景验证恭喜获得XX积分这种动态文案 import re # 方式一正则等待出现 poco(textMatches恭喜获得\d积分).wait_for_appearance(timeout10) # 方式二获取并提取数字 element poco(textMatches恭喜获得\d积分) if element.exists(): text element.get_text() # 提取数字 import re match re.search(r\d, text) if match: score int(match.group()) print(f获得积分{score}) assert score 0, 积分应大于06.6 案例六表单填写通用函数pythondef fill_form(fields_dict): 通用表单填写函数 fields_dict { 姓名: 张三, 手机号: 13800138000, 地址: 北京市朝阳区 } for label, value in fields_dict.items(): # 找到标签对应的输入框 label_element poco(textlabel) if not label_element.exists(): print(f未找到字段{label}) continue # 找到标签后面的输入框同级或子级 parent label_element.parent() edit_text parent.offspring(android.widget.EditText) if edit_text.exists(): edit_text.set_text(value) print(f填写 {label} {value}) else: print(f未找到 {label} 的输入框) # 使用 fill_form({ 姓名: 张三, 手机号: 13800138000, 地址: 北京市朝阳区望京SOHO })七、Airtest Poco 混合编程最佳实践7.1 推荐的脚本结构python# -*- encodingutf8 -*- __author__ 测试工程师 from airtest.core.api import * from airtest.report.report import simple_report from poco.drivers.android.uiautomation import AndroidUiautomationPoco # 1. 初始化 auto_setup(__file__) poco AndroidUiautomationPoco() # 2. 全局等待时间 TIMEOUT 30 # 3. 自定义工具函数 def wait_and_click(element, msg): 等待元素出现并点击 if isinstance(element, str): element poco(textelement) if element.wait_for_appearance(timeoutTIMEOUT): element.click() print(f点击了{msg or element.get_text()}) return True else: snapshot(msgf未找到元素{msg}) return False def handle_unexpected_popup(): 处理意外弹窗 popup_patterns [ poco(textMatches.*(更新|升级).*), poco(textMatches.*(允许|授权).*), poco(textMatches.*(温馨提示|公告).*), ] for pattern in popup_patterns: if pattern.exists(): # 优先找关闭/取消按钮 for close_text in [关闭, 取消, 我知道了, ×]: close_btn pattern.parent().offspring(textclose_text) if close_btn.exists(): close_btn.click() sleep(1) return True return False # 4. 测试用例 class TestShoppingApp: def test_login(self): 登录测试 # 使用Poco等待页面 poco(text我的).wait_for_appearance(timeoutTIMEOUT) # 使用Poco点击 wait_and_click(poco(text登录/注册), 登录入口) # 处理可能的弹窗 handle_unexpected_popup() # 输入账号密码Poco poco(android.widget.EditText)[0].set_text(testuser) poco(android.widget.EditText)[1].set_text(123456) # 点击登录按钮Poco poco(text登录).click() # 验证登录成功Poco等待 Airtest断言 poco(text我的订单).wait_for_appearance(timeout10) # 截图断言Airtest assert_exists(Template(rlogin_success.png, record_pos(0, -0.2), resolution(1080, 2340)), 登录成功页面验证) def test_search_product(self): 搜索商品测试 # 使用Poco定位搜索框 search_box poco(com.example.app:id/search_box) search_box.wait_for_appearance(timeoutTIMEOUT) search_box.click() # 输入搜索词 search_box.set_text(手机) # 点击搜索按钮 poco(text搜索).click() # 等待结果加载 poco(textMatches.*个结果.*).wait_for_appearance(timeout10) # 验证有搜索结果 products poco(com.example.app:id/product_item) assert len(products) 0, 搜索结果不应为空 # 截图验证 snapshot(msg搜索结果页) # 5. 执行 if __name__ __main__: test TestShoppingApp() test.test_login() sleep(2) test.test_search_product() # 生成报告 simple_report(__file__, logpathTrue)7.2 分工原则总结┌────────────────────────────────────────────────┐ │ 脚本任务分工 │ ├───────────────────┬────────────────────────────┤ │ Poco负责 │ Airtest负责 │ ├───────────────────┼────────────────────────────┤ │ ✅ 控件定位与点击 │ ✅ 启动/关闭APP │ │ ✅ 文本输入 │ ✅ 截图记录 │ │ ✅ 获取文本/属性 │ ✅ 断言验证 │ │ ✅ 等待元素出现 │ ✅ 生成测试报告 │ │ ✅ 遍历控件列表 │ ✅ 图像识别兜底 │ │ ✅ 处理动态内容 │ ✅ 全局滑动操作 │ │ ✅ 控件状态判断 │ ✅ keyevent按键操作 │ └───────────────────┴────────────────────────────┘八、常见问题与排坑指南8.1 元素找不到最高频python# 问题poco(text登录).click() 报错找不到元素 # 排查步骤 # 1. 检查控件是否真的存在 print(poco(text登录).exists()) # True/False # 2. 用辅助窗检查控件属性 # 打开Poco辅助窗 → 点击目标元素 → 看属性面板 # 确认 text 属性值是否是 登录可能有空格/换行 # 3. 检查是否有多个同名控件 print(len(poco(text登录))) # 如果有多个用索引 # 4. 检查控件是否被遮挡/不可见 print(poco(text登录).attr(visible)) # 应为 True # 5. 改用正则匹配 poco(textMatches.*登录.*) # 6. 检查是否需要先滚动到可见区域 poco(text登录).focus() # 尝试聚焦 # 7. 最后手段用图像识别兜底 touch(Template(rlogin_button.png))8.2 文本输入无效python# 问题set_text() 后输入框没变化 # 方案一先点击再输入 poco(EditText).click() sleep(0.5) poco(EditText).set_text(hello) # 方案二用keyevent逐字符输入兜底 text hello poco(EditText).click() for char in text: keyevent(char) # 方案三清空后再输入 poco(EditText).set_text() # 先清空 sleep(0.3) poco(EditText).set_text(hello)8.3 动态ID处理python# 问题每次启动APP控件ID都会变如 com.xxx:id/btn_xxxxx # 方案一用text属性代替ID poco(text登录).click() # 替代 poco(com.xxx:id/btn_12345) # 方案二正则匹配ID poco(textMatches.*登录.*) # 方案三用相对定位 poco(Toolbar).child(Button).click() # 方案四用desc属性 poco(desc登录按钮).click()8.4 WebView/H5元素获取不到python# 问题APP内嵌H5页面Poco看不到WebView内的元素 # 方案一切换到WebView模式Android from poco.drivers.android.uiautomation import AndroidUiautomationPoco poco AndroidUiautomationPoco(use_webviewTrue) # 方案二用Chrome DevTools调试WebView # 1. 开启APP的WebView调试 # 2. Chrome访问 chrome://inspect # 3. 找到目标WebView → inspect # 方案三用图像识别兜底 touch(Template(rh5_button.png))8.5 滑动不生效python# 问题poco(ScrollView).swipe() 滑动无效 # 排查 # 1. 确认是ScrollView控件 print(poco(ScrollView).get_name()) # 2. 确认控件可滑动 # 在辅助窗中查看控件属性 # 3. 调整滑动参数百分比坐标 poco(ScrollView).swipe([0.5, 0.7], [0.5, 0.3], duration1.0) # 4. 用Airtest全局滑动兜底 swipe([540, 1500], [540, 500], duration1.0)8.6 控件状态同步问题python# 问题点击后立刻读取属性拿到的是旧值 # 解决加适当的等待 poco(text开关).click() sleep(0.5) # 等待UI更新 assert poco(text开关).attr(checked) True # 更好的方式等待属性变化 poco(text开关).click() poco(text开关, checkedTrue).wait_for_appearance(timeout5)九、Poco在不同平台的配置9.1 Android最常用python# Android 默认模式 from poco.drivers.android.uiautomation import AndroidUiautomationPoco poco AndroidUiautomationPoco() # 指定设备 poco AndroidUiautomationPoco(deviceemulator-5554) # 开启WebView支持 poco AndroidUiautomationPoco(use_webviewTrue) # 调整等待轮询间隔 poco AndroidUiautomationPoco(screenshot_each_actionFalse)9.2 iOSpython# iOS 需要先启动WebDriverAgent from poco.drivers.ios import iosPoco poco iosPoco() # 指定设备 poco iosPoco(deviceiPhone)9.3 Unity游戏python# Unity 需要在项目中集成Poco-SDK from poco.drivers.unity3d import UnityPoco poco UnityPoco() # 连接指定端口 poco UnityPoco((localhost, 5001))9.4 Cocos2d游戏pythonfrom poco.drivers.cocos2dx import Cocos2dxPoco poco Cocos2dxPoco()9.5 微信小程序python# 小程序需要先开启调试模式 # 1. 小程序右上角 → 开发调试 → 开启调试 # 2. 微信扫码授权Airtest IDE from poco.drivers.wechat.miniprogram import MiniProgramPoco poco MiniProgramPoco()十、总结核心API速查表分类API说明定位poco(text登录)基本选择器poco(父).child(子)相对选择器poco(textMatches.*正则.*)正则选择器poco(Button)[0]索引选择操作.click()点击.long_click()长按.swipe([x1,y1],[x2,y2])滑动.set_text(内容)输入文本.drag_to(目标)拖拽查询.get_text()获取文本.attr(属性名)获取属性.exists()是否存在.get_position()获取位置等待.wait_for_appearance()等待出现.wait_for_disappearance()等待消失.wait(timeout)等待超时报错批量poco.wait_for_all([...])等待全部出现poco.wait_for_any([...])等待任一出现技能自检清单能在Poco辅助窗中找到目标控件并查看属性能用基本选择器定位控件poco(textxxx)能用相对选择器通过层级关系定位能用正则表达式匹配动态文本能遍历控件列表并获取每个元素的信息能使用wait_for_appearance()处理异步加载能判断控件是否存在exists()并做分支处理能处理随机弹窗知道何时用Poco、何时用Airtest能排查找不到元素类问题记忆口诀Poco定位三板斧基本选、相对链、正则可 操作六兄弟点击、长按、滑动、输入、拖拽、获取 等待三件套出现、消失、超时等 查属性用attr判存在用exists Airtest管截图断言Poco管控件操作 找不到元素不要慌属性、层级、正则依次试下一篇将进入实战——APP自动化测试完整项目我们会把Airtest图像识别和Poco控件识别结合起来从头到尾构建一个真正的APP自动化测试工程敬请期待