Airtest Web端UI自动化实战:图像识别与Poco控件树双模驱动
1. 项目概述为什么选择Airtest做Web端UI自动化最近在团队里讨论UI自动化测试框架选型特别是针对Web端大家提得最多的还是Selenium、Playwright或者Cypress。但我提了一嘴“用Airtest试试”不少同事都愣了一下第一反应是“Airtest那不是专门做游戏和App自动化的吗” 没错Airtest在移动端和游戏测试领域确实名声在外但很多人忽略了它其实是一个基于图像识别和Poco控件识别的跨平台自动化框架对Web端同样有不错的支持。我之所以想把这个“非主流”方案拿出来实战是因为在实际项目中我们遇到了几个痛点一是测试环境复杂被测系统经常内嵌大量Canvas绘图、动态图表或者非标准Web组件传统基于DOM的定位方式如XPath、CSS Selector经常失效或变得极其脆弱二是团队里既有测App的同事也有测Web的大家不想维护两套完全不同的技术栈和脚本三是我们希望有一些“所见即所得”的录制回放或快速编写脚本的能力降低自动化门槛。Airtest的核心优势在于它的“双模驱动”图像识别和Poco UI控件树识别。对于Web端我们可以通过Poco的Web驱动像操作App原生控件一样去定位和操作网页元素。同时当控件定位失灵时比如那个永远在变的验证码区域可以直接祭出图像识别这个大招截个图就能点。这种灵活性在处理一些“顽固”的Web页面时往往有奇效。这次实战我就带大家走一遍完整的流程从环境搭建、驱动配置到脚本编写、调试技巧最后再聊聊如何融入持续集成。你会发现用Airtest做Web自动化不仅可行在某些场景下甚至更省心。2. 环境搭建与核心驱动配置2.1 基础Python环境与Airtest IDE安装工欲善其事必先利其器。Airtest提供了两种主要的使用方式独立的Airtest IDE内置了录制、回放、脚本编辑和报告生成和纯Python库的方式。对于新手或者想快速验证、录制脚本的测试同学我强烈推荐从Airtest IDE开始。首先去Airtest官网下载对应操作系统的Airtest IDE安装包。安装过程很简单一路下一步就行。安装完成后打开你会看到一个简洁的界面中间是设备连接区右边是脚本编辑区。Airtest IDE内置了Python环境所以你不需要额外操心Python版本和包依赖的问题这对新手极其友好。如果你想在命令行或者自己的CI/CD环境中运行脚本那就需要搭建Python环境。建议使用Python 3.7-3.9版本兼容性最好。创建一个虚拟环境后用pip安装核心包pip install airtest这个命令会安装Airtest核心库以及Poco框架。但请注意这不包含Web自动化所需的浏览器驱动。Airtest IDE里集成了Chromedriver但独立安装时我们需要自己处理。2.2 关键一步配置Web自动化驱动Poco-Web这是Airtest做Web自动化的核心也是最容易卡住新手的地方。Airtest通过Poco框架来识别Web控件而Poco需要借助一个“桥梁”来连接浏览器和我们的脚本。这个桥梁就是poco-web驱动。驱动安装与注入原理Poco-Web驱动本质上是一个JavaScript文件它需要被注入到待测试的网页中在浏览器端运行负责扫描页面的DOM结构并将其转换为一棵Poco能够理解的UI控件树然后通过WebSocket与我们的Python脚本通信。安装驱动库pip install pocouipocoui包包含了Poco框架以及各种平台的驱动其中就有Web驱动。但是仅仅安装Python包还不够。我们还需要让浏览器加载那个关键的JS文件。有两种主流方式方式一使用Airtest IDE自动注入推荐新手这是最简单的方法。在Airtest IDE中你可以直接连接一个Chrome浏览器实例。IDE在启动浏览器时会自动通过--load-extension参数加载一个插件这个插件负责向每一个打开的页面注入poco-web的JS驱动。你基本无需手动干预连接后就能直接用Poco定位元素。方式二手动启动浏览器并注入驱动适用于CI/CD在无头环境或者需要精确控制浏览器启动参数的场景下我们需要手动操作。这里以Chrome为例下载驱动JS文件你需要找到poco-web.js文件。它通常位于你Python环境下的site-packages/airtest/core/web目录中或者pocoui库的安装目录里。把它复制到你的项目目录下。编写启动脚本使用Selenium WebDriver启动Chrome并在启动参数中指定加载这个JS文件作为扩展或者通过execute_cdp_cmd命令在页面加载后注入。from selenium import webdriver from selenium.webdriver.chrome.options import Options import os # 设置Chrome选项 chrome_options Options() # 无头模式适合CI环境 # chrome_options.add_argument(--headless) chrome_options.add_argument(--disable-gpu) # 禁用沙盒在某些Linux环境下可能需要 chrome_options.add_argument(--no-sandbox) # 允许跨域有时必要 chrome_options.add_argument(--disable-web-security) # 非常重要指定加载包含poco-web驱动的扩展 # 假设你把poco-web.js和相关manifest文件打包成了一个Chrome扩展放在./poco_web_extension目录 chrome_options.add_argument(--load-extension os.path.abspath(./poco_web_extension)) driver webdriver.Chrome(optionschrome_options) driver.get(http://your-test-site.com)手动打包扩展对于初学者有点复杂。更常见的做法是在Airtest脚本中使用connect_device连接一个已经由Airtest IDE或脚本启动的、已经注入了驱动的浏览器窗口。或者使用Airtest提供的chrome://inspect调试端口连接方式。实操心得对于团队初次使用强烈建议先用Airtest IDE的自动连接功能把整个流程跑通理解Poco是如何工作的。然后再去研究如何在CI中无头运行。直接上手搞命令行注入很容易被各种路径和浏览器安全策略搞崩溃。2.3 连接设备与初始化Poco对象环境准备好后下一步就是连接浏览器并初始化Poco。在Airtest IDE中你可以点击设备窗口的“连接”按钮选择“浏览器”然后输入URLIDE会自动帮你完成连接和Poco初始化。在纯Python脚本中步骤会稍微多几步from airtest.core.api import * from poco.drivers.web.web import WebPoco import time # 1. 使用Airtest的start_app方法启动浏览器需要预先知道浏览器可执行文件路径 # 这种方式Airtest会自动尝试注入驱动 # start_app(C:\Program Files\Google\Chrome\Application\chrome.exe, args[f--apphttp://www.baidu.com]) # 2. 更通用的方式使用connect_device连接一个已有的浏览器窗口 # 首先你需要用上述手动方式启动一个注入了驱动的浏览器并记住其调试端口号例如9222 # 启动Chrome时加上--remote-debugging-port9222 # 然后连接 connect_device(Windows:///?title_re.*Chrome.*) # 通过窗口标题连接不稳定 # 或者更好的方式通过调试端口连接需安装airtest的web扩展支持 # dev connect_device(http://localhost:9222) # 这里通常需要一些自定义代码因为Airtest对Web的connect_device支持不如移动端完善 # 3. 初始化Poco对象以连接成功后为例 # 假设我们已经成功连接了一个浏览器设备并赋值给变量 dev # poco WebPoco(dev) # 在实际中更常见的写法是直接初始化因为Poco会尝试查找当前活动的设备 poco WebPoco() time.sleep(3) # 等待页面和Poco驱动完全加载 # 现在你就可以使用poco对象来定位和操作页面元素了 poco(input#kw).set_text(Airtest Web自动化) poco(input#su).click()注意事项初始化Poco对象后最好加一个短暂的等待time.sleep(3)或使用poco的等待方法。这是因为注入的JS驱动需要时间扫描和构建整个页面的控件树。如果立即进行操作可能会找不到元素。3. 核心定位策略图像识别与Poco控件树的双剑合璧Airtest做Web自动化的精髓就在于“两条腿走路”。当一条路走不通时立刻换另一条总有一种方法能让你操作到目标元素。3.1 Poco控件定位精准且可维护Poco定位是首选因为它基于DOM结构稳定且执行速度快。它的语法非常直观类似于jQuery或CSS选择器。基本定位方法通过属性定位这是最常用的方式。可以直接使用poco(“tagName#id”)或poco(‘.className’)。poco(“input#username”).set_text(“admin”) # id定位 poco(“.btn-submit”).click() # class定位 poco(“a[href’/logout’]”).click() # 属性定位通过文本定位对于链接、按钮等有明确文字的元素非常方便。poco(text”登录”).click() poco(textMatches”^保存.*”).click() # 正则匹配层级与相对定位当元素没有唯一标识时可以通过父子、兄弟关系定位。# 先定位父元素再找子元素 form poco(“form#loginForm”) form.child(“input”)[0].set_text(“user”) # 第一个input子元素 # 使用offspring获取所有后代元素 poco(“div.content”).offspring(“button”)[1].click()等待元素出现自动化脚本必须健壮等待是关键。Poco提供了好用的等待API。# 等待元素出现最多等10秒 btn poco(“button#submit”).wait(10).click() # 等待元素消失常用于等待加载动画结束 poco(“div.loading”).wait_for_disappearance(timeout10)实操心得尽量使用id、name或者独特的>from airtest.core.api import * # 假设你已经截取了一个“登录按钮”的图片保存为login_btn.png touch(Template(r”login_btn.png”)) # 点击图片所在位置 # 带可信度匹配 if exists(Template(r”login_btn.png”, threshold0.9)): # threshold为匹配阈值0-1 touch(Template(r”login_btn.png”)) else: print(“未找到登录按钮”)图像识别的进阶技巧ROI区域限定如果全屏搜索速度慢或者容易误匹配可以指定搜索区域。# 在屏幕坐标 (x1, y1) 到 (x2, y2) 的矩形区域内搜索 pos exists(Template(r”icon.png”, roi[100, 200, 300, 400])) # [x1, y1, x2, y2]多分辨率适配你的脚本可能在不同分辨率的机器上运行。Airtest的Template默认支持一定程度的尺度不变性但对于差异过大的分辨率最好准备多套截图或者使用resolution参数进行适配需要更复杂的设置。处理动态内容对于内容会变但样式不变的区域如验证码图片本身可以尝试截取其外围的静态容器框进行定位。注意事项图像识别受屏幕分辨率、缩放比例、颜色主题、甚至字体渲染的影响。它不是百分百可靠的。最佳实践是以Poco控件定位为主图像识别为辅。图像识别更适合用于验证页面是否成功跳转判断某个特征图片出现。操作那些无法通过控件树获取的“纯图片”按钮或区域。在脚本调试阶段快速点击某个已知位置。3.3 混合定位策略实战案例假设我们有一个后台管理系统其数据表格是第三方JS库生成的DOM结构非常复杂且动态变化Poco定位很困难。但表格的“导出Excel”按钮是一个固定的图标。我们可以这样操作def export_table_data(): # 步骤1用Poco定位到表格所在的父容器这个容器相对稳定 table_container poco(“div.ag-theme-balham”).wait(10) # 步骤2在容器内用Poco尝试定位“刷新”按钮假设它有固定id poco(“button#refresh-table”).click() # 步骤3等待数据加载完成假设加载时会有蒙层 poco(“div.loading-overlay”).wait_for_disappearance(timeout30) # 步骤4导出按钮是图片Poco定位不到使用图像识别 # 我们先确保表格区域在可视范围内 table_container.scroll_to_view() # 截取导出按钮的图片存为 export_icon.png try: touch(Template(r”export_icon.png”, roitable_container.area)) # 将搜索区域限定在表格容器内 print(“点击导出按钮成功”) except TargetNotFoundError: print(“未找到导出按钮尝试备用方案...”) # 备用方案通过键盘快捷键或者菜单栏的Poco定位 poco(“menu”).child(“item”, text”导出”).click() poco(“menu”).child(“item”, text”Excel格式”).click()这种混合策略极大地提升了脚本的健壮性。4. 常用操作与脚本结构设计掌握了定位接下来就是一系列的操作。Airtest (Poco) 提供了一套完整的操作API。4.1 基本输入与点击# 输入文本 poco(“input#email”).set_text(“testexample.com”) # 清空后输入 poco(“input#search”).set_text(“”).set_text(“new keyword”) # 点击 poco(“button”).click() # 长按模拟鼠标按下 poco(“draggable-item”).long_click(duration2)4.2 鼠标悬停与滚动Web测试中悬停显示菜单和滚动加载非常常见。# 鼠标悬停 poco(“div.user-avatar”).hover() # 可能需要WebPoco特定驱动支持 # 等待悬停菜单出现 poco(“ul.dropdown-menu”).wait(5).click(“li”, text”Profile”) # 滚动操作 # 滚动到某个元素可见 poco(“footer”).scroll_to_view() # 在某个元素内滚动如一个可滚动div scroll_view poco(“div.scrollable-area”) scroll_view.scroll(direction’vertical’, percent0.8) # 向下滚动80% # 模拟鼠标滚轮通过airtest的swipe模拟 swipe([500, 300], [500, 200]) # 从(500,300)向上滑动到(500,200)模拟向上滚动4.3 断言与验证自动化测试离不开断言。# 断言元素存在 assert poco(“h1.page-title”).exists() # 断言文本内容 assert_equal(poco(“div.status”).get_text(), “操作成功”) # 断言元素属性 assert_equal(poco(“input”).attr(‘value’), “预期值”) # 使用Airtest的assert_exists进行图像断言 assert_exists(Template(r”success_toast.png”), “成功提示未出现”)4.4 脚本结构设计与封装为了可维护性绝对不能把所有操作堆在一个线性脚本里。要借鉴编程中的模块化思想。1. 页面对象模型Page Object封装这是UI自动化的最佳实践。将每个页面或功能模块封装成一个类。# login_page.py class LoginPage: def __init__(self, poco): self.poco poco self.username_input “input#username” self.password_input “input#password” self.submit_button “button[type’submit’]” self.error_msg “div.alert-error” def login(self, username, password): self.poco(self.username_input).set_text(username) self.poco(self.password_input).set_text(password) self.poco(self.submit_button).click() def get_error_message(self): if self.poco(self.error_msg).exists(): return self.poco(self.error_msg).get_text() return None # 在测试脚本中使用 from login_page import LoginPage login_page LoginPage(poco) login_page.login(“admin”, “wrongpass”) assert “密码错误” in login_page.get_error_message()2. 关键操作封装与重试机制对于不稳定的操作如网络请求后的元素加载加入重试逻辑。from airtest.core.api import * from poco.exceptions import PocoNoSuchNodeException import time def click_with_retry(poco, locator, retries3, interval2): “””带重试的点击操作””” for i in range(retries): try: element poco(locator).wait(interval) element.click() print(f”成功点击 {locator}”) return True except (PocoNoSuchNodeException, TargetNotFoundError): print(f”第{i1}次尝试点击 {locator} 失败等待后重试...”) time.sleep(interval) print(f”点击 {locator} 失败已达最大重试次数”) return False # 使用 click_with_retry(poco, “button#submit”, retries5)3. 数据驱动测试将测试数据与脚本逻辑分离。import csv import pytest def load_test_data(): with open(‘test_data.csv’, ‘r’, encoding’utf-8′) as f: reader csv.DictReader(f) return list(reader) pytest.mark.parametrize(“data”, load_test_data()) def test_login_with_different_users(poco, data): login_page LoginPage(poco) login_page.login(data[‘username’], data[‘password’]) if data[‘expected_success’] ‘true’: assert poco(“div.dashboard”).exists() else: assert login_page.get_error_message() is not None5. 调试技巧与常见问题排查即使方案再完美调试也是自动化过程中最耗时的一环。分享几个我踩过坑后总结的实用技巧。5.1 利用Airtest IDE的调试工具Poco Inspector控件检视器这是你最好的朋友。在IDE里连接设备后点击Poco辅助窗的“刷新”按钮就能看到实时的控件树。你可以点击树上的节点页面上对应的元素会高亮。更重要的是你可以直接右键节点复制它的定位语句如poco(“XXX”)直接粘贴到脚本里使用极大提升了编写效率。Airtest辅助窗用于图像识别。你可以随时截取屏幕上的任意区域保存为模板图片并立即在脚本中生成对应的Template代码。还能调整匹配阈值threshold实时预览匹配效果。脚本录制对于不熟悉Poco语法的同学可以先用录制功能。在IDE中点击录制然后手动在浏览器上操作一遍IDE会自动生成操作代码。虽然生成的代码可能不够优化但作为一个快速的起点和语法参考非常有用。5.2 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案Poco定位不到元素1. 页面未完全加载。2. 元素在iframe内。3. 元素属性动态变化。4. Poco驱动未成功注入。1. 增加wait时间或等待特定元素出现。2. 使用poco(‘iframe’).poco(‘内部元素’)切换到iframe。3. 使用更稳定的定位方式如text、id或改用部分匹配textMatches。4. 检查浏览器控制台是否有Poco相关的JS错误重新连接设备。图像识别匹配失败1. 截图与当前屏幕分辨率/缩放比不一致。2. 页面内容、主题、字体发生变化。3. 匹配阈值 (threshold) 设置过高。1. 尽量在同分辨率环境下录制和运行脚本尝试使用resolution参数适配。2. 更新模板图片尝试截取更具唯一性、更不易变的区域。3. 适当降低threshold(如从0.9调到0.8)但需注意误匹配风险。操作执行了但没效果1. 元素被遮挡如弹窗、遮罩层。2. 元素状态不可点击disabled。3. 点击坐标偏移。1. 先关闭或点击关闭弹窗使用poco(‘元素’).wait_for_appearance()确保元素可交互。2. 在操作前用poco(‘元素’).attr(‘disabled’)判断状态。3. 对于图像识别检查target_pos参数点击图片的第几个点默认是5即中心点。脚本在CI环境失败本地却成功1. CI环境是无头模式渲染或尺寸可能不同。2. CI环境网络或资源加载慢。3. 路径或环境变量问题。1. 在CI脚本中增加浏览器窗口大小设置chrome_options.add_argument(‘–window-size1920,1080’)。2. 大幅增加全局等待时间在关键步骤后添加显式等待。3. 使用绝对路径引用模板图片在CI脚本中打印当前工作目录和文件列表进行核对。性能慢执行时间长1. 图像识别全屏搜索。2. 等待时间设置过长且固定。3. 控件树过于庞大。1. 为Template指定roi参数缩小搜索范围。2. 将固定的time.sleep改为智能等待如poco(…).wait。3. 如果可能尝试定位更靠近目标的父元素然后在其后代中搜索。5.3 日志与报告分析Airtest在运行时会生成丰富的日志和截图。运行脚本时注意观察Airtest IDE的运行日志窗口里面会详细记录每一步操作、定位结果和成功/失败信息。最重要的是生成HTML测试报告from airtest.report.report import simple_report # … 运行你的测试脚本 … simple_report(__file__, logpathTrue, outputr”./log.html”)报告里会按时间线展示每一步的操作、截图和结果。任何失败步骤都会高亮显示并附上当时的屏幕截图这对于排查“为什么点击没反应”这类问题至关重要。养成每次运行后查看报告的习惯能快速定位问题所在。6. 融入持续集成CI流程脚本最终要能无人值守地运行集成到CI/CD管道中是必然。这里以Jenkins为例给出一个可行的思路。核心挑战CI服务器通常是无图形界面的Linux环境而Airtest的图像识别和浏览器自动化需要图形环境。解决方案使用虚拟显示服务器Xvfb在无头Linux上模拟一个显示设备。# 在Jenkins的Shell构建步骤中 # 1. 安装Xvfb和必要的库 apt-get install -y xvfb libgtk-3-0 libxss1 libgconf-2-4 libnss3 libasound2 # 2. 启动Xvfb指定显示编号和分辨率 Xvfb :99 -screen 0 1920x1080x24 export DISPLAY:99 # 3. 在此环境下运行你的Airtest Python脚本 python your_web_auto_test.py配置Headless Chrome与驱动注入在CI脚本中以无头模式启动Chrome并确保Poco-Web驱动被正确注入。这可能需要你将驱动JS文件打包成扩展并在启动Chrome时通过–load-extension加载或者研究使用chrome.debuggerAPI动态注入。脚本与资源管理确保你的测试脚本、模板图片、测试数据文件在CI工作空间中的路径是正确的。建议使用绝对路径或者通过环境变量来配置资源根目录。结果收集与通知在脚本最后使用simple_report生成HTML报告。然后可以使用Jenkins的插件如HTML Publisher plugin发布这个报告。同时脚本的退出码0成功非0失败应正确反映测试结果以便Jenkins判断构建状态。可以将报告链接通过邮件或钉钉/企业微信等机器人发送给团队。一个简化的CI脚本骨架可能如下# ci_run.py import sys import os from airtest.core.api import * from poco.drivers.web.web import WebPoco from selenium import webdriver from selenium.webdriver.chrome.options import Options def setup_browser(): chrome_options Options() chrome_options.add_argument(‘–headless’) chrome_options.add_argument(‘–no-sandbox’) chrome_options.add_argument(‘–disable-dev-shm-usage’) chrome_options.add_argument(‘–disable-gpu’) chrome_options.add_argument(‘–window-size1920,1080’) # 关键加载包含poco-web驱动的扩展 extension_path os.path.join(os.path.dirname(__file__), ‘poco_web_extension’) chrome_options.add_argument(‘–load-extension’ extension_path) driver webdriver.Chrome(optionschrome_options) return driver def main(): try: driver setup_browser() driver.get(“http://your-test-env.com”) # 这里需要一些自定义代码来将driver实例与Airtest/Poco连接 # 可能需要用到 airtest的 connect_device 或直接初始化Poco # poco WebPoco(driver) # 这通常需要额外的适配层 # … 执行你的测试用例 … test_result “SUCCESS” except Exception as e: print(f”测试执行失败: {e}”) test_result “FAILURE” sys.exit(1) finally: if ‘driver’ in locals(): driver.quit() # 生成报告 from airtest.report.report import simple_report simple_report(__file__, logpathTrue, outputos.path.join(os.getcwd(), “log.html”)) if test_result “SUCCESS”: sys.exit(0) else: sys.exit(1) if __name__ “__main__”: main()踩坑提醒将Airtest Web自动化集成到CI最大的难点就是无头环境下的驱动注入和连接。社区中现成的、开箱即用的方案不多可能需要你根据实际情况进行一些定制化开发比如编写一个适配层将Selenium WebDriver实例“包装”成Airtest可识别的设备。这部分需要一定的耐心和调试能力。