1. 项目概述为什么我们需要Airtest这样的UI自动化框架如果你做过移动端或者PC端的测试尤其是涉及到界面交互的测试大概率会对“点点点”感到厌倦。手动测试不仅耗时耗力而且重复性高容易因为疲劳而出错。更头疼的是现在的应用往往是跨平台的——同一个业务逻辑需要在Android、iOS、Windows等多个操作系统上验证。传统的自动化测试工具要么绑定单一平台比如Android的UIAutomator要么学习曲线陡峭、脚本维护成本高比如基于Selenium的WebDriver协议扩展。这时候Airtest就进入了我们的视野。我第一次接触Airtest是在一个手游项目的测试中团队需要在几十台不同型号的安卓设备上跑相同的测试用例。当时尝试过一些基于图像识别的工具但要么识别率感人要么对设备环境要求苛刻。Airtest的出现算是解决了这个痛点。它本质上是一个跨平台的UI自动化测试框架核心卖点在于其“图像识别”和“跨平台”能力。你不需要深入理解每个平台底层复杂的UI控件树通过截图就能定位元素写一套脚本理论上可以在Android、iOS、Windows甚至嵌入式设备上运行。这听起来有点像“银弹”但实际用下来它确实在特定场景下极大地提升了效率。它尤其适合游戏测试、应用GUI测试以及一些对控件识别困难比如大量使用自定义绘制、游戏引擎渲染的场景。接下来我会结合我多年的使用和趟坑经验为你深度拆解Airtest从设计思路到实操细节再到那些官方文档里不会写的避坑指南。2. Airtest整体设计与核心思路拆解2.1 双引擎驱动图像识别与Poco控件识别Airtest的架构设计非常巧妙它没有把自己局限在单一技术上而是提供了两套并行的“引擎”来驱动自动化。第一引擎基于Sikuli的图像识别。这是Airtest最广为人知的特点。它的原理并不复杂你在脚本里插入一张你希望点击的按钮的截图Airtest运行时会在当前设备屏幕上实时截图通过算法默认是模板匹配在你的截图和屏幕截图中寻找相似度最高的区域然后对这个区域中心点执行点击操作。这种方式完全“无视”平台和控件类型只要屏幕上“看起来一样”就能操作。这对于游戏界面Unity、Cocos、Flutter应用或者一些深度定制的UI来说是救命稻草。注意图像识别并非万能。它对屏幕分辨率、缩放比例、颜色、光照甚至抗锯齿效果都敏感。同一张截图在1080P和2K屏上匹配结果可能天差地别。因此它更适合相对稳定的UI界面。第二引擎基于Poco的UI控件树识别。这是对图像识别的有力补充。Poco可以理解为Airtest框架内的一个“控件探测器”。它通过接入不同平台的访问接口如Android的UIAutomator iOS的XCUITest Windows应用的Accessibility API 游戏的游戏引擎接口获取当前界面的结构化控件树。然后你可以像使用jQuery选择器一样通过控件的属性name, type, pos来定位和操作它。这种方式精准、稳定且脚本可读性高。为什么是双引擎这是一种务实的折中。纯图像识别不稳定纯控件识别在某些场景如游戏、Canvas绘制下不可用。Airtest让你可以根据实际情况混合使用。例如对于一个原生Android应用你可以用Poco精准定位登录按钮而对于游戏里的一个技能图标你可以用图像识别去点击。这种灵活性是它跨平台能力的基石。2.2 跨平台实现的底层逻辑“写一次跑在任何地方”是美好的愿景但底层实现异常复杂。Airtest的跨平台并不是说用同一套二进制代码在所有平台运行而是通过分层架构和统一API来实现的。脚本层统一你使用AirtestIDE或者纯Python编写测试脚本。这一层的API是统一的例如touch(图片)、poco(‘btn_login’).click()。核心框架层Airtest核心框架负责解析你的脚本命令并调用对应的“设备操作模块”。设备操作层平台适配层这是关键。Airtest为每个支持的平台Android, iOS, Windows, Unity, Cocos等实现了独立的“设备代理”。当你连接一台Android手机时框架会加载Android对应的模块这个模块会通过ADBAndroid Debug Bridge协议与手机通信实现截图、点击、滑动等操作。连接Windows窗口时则会调用Windows相关的API如pywin32。图像识别引擎这是一个相对独立的服务接收来自设备操作层的截图进行图像处理与匹配将坐标结果返回。所以当你从测试Android切换到测试Windows应用时你不需要修改脚本逻辑只需要在初始化时指定不同的设备连接底层框架会自动切换通信协议和操作方式。这种设计牺牲了一定的执行效率因为多了一层抽象但换来了无与伦比的脚本复用性和开发效率。2.3 AirtestIDE一体化的开发与调试环境官方提供的AirtestIDE是一个基于Python和Qt开发的集成工具它极大地降低了使用门槛。它不仅仅是一个编辑器更是一个强大的“所见即所得”的脚本录制与调试环境。设备屏幕实时投屏直接在你的电脑上显示设备画面延迟很低。可视化脚本录制你可以用鼠标在投屏画面上点击IDE会自动生成对应的图像识别语句或Poco定位语句。这对于快速创建测试用例原型非常有用。Poco Inspector可以实时查看当前界面的控件树并像浏览器开发者工具一样检查控件属性方便你编写精准的Poco选择器。脚本运行与报告查看一键运行脚本并自动生成图文并茂的HTML测试报告每一步操作都有截图失败的地方高亮显示。对于新手我强烈建议从AirtestIDE开始。它能帮你快速理解框架的工作模式并验证你的操作逻辑是否正确。但在大型项目或持续集成中我们通常还是会回归到纯Python脚本的模式以便于版本管理和集成。3. 核心细节解析与实操要点3.1 图像识别的艺术如何让touch(图片)更稳定图像识别是Airtest的入门砖也是最容易踩坑的地方。很多人抱怨“识别不准”、“有时候能点到有时候点不到”问题往往出在截图的“质量”和匹配参数的“调校”上。1. 截图采集的“黄金法则”从真实设备截取而非设计稿务必在目标设备或相同分辨率、DPI的设备上运行应用然后通过AirtestIDE的截图功能截取。设计稿的尺寸、颜色、阴影效果和真实渲染结果常有差异。截取特征鲜明的区域避免截取大面积的、纯色或重复纹理的区域。应该截取图标、文字、按钮等具有独特视觉特征的部分。例如截取一个完整的、带有边框和文字的按钮而不是只截取按钮中间的图标。控制截图尺寸截图不是越大越好。太大的截图包含过多冗余信息降低匹配速度且可能引入干扰太小则特征不足。通常截取目标元素的整个视觉区域四周留一点点边缘即可。2. 关键参数调校touch函数背后是assert_exists匹配可以传入threshold阈值和target_pos点击位置等参数。# 示例提高匹配阈值并点击目标区域的第2个点位共9宫格 touch(Template(r”tpl123.png”, threshold0.9, target_pos2))threshold匹配阈值默认0.7这是置信度。值越高要求匹配度越高越严格。如果UI经常有细微变化如颜色渐变、动态效果可以适当降低到0.6-0.65。对于非常稳定的图标可以提高到0.9以上。这是一个需要根据实际情况反复调试的参数。target_pos点击点位默认是5即匹配区域的中心。有时按钮可点击区域并非图像中心你可以通过此参数调整。取值1-9对应九宫格位置。rgb参数默认为False进行灰度匹配。如果你的UI色彩信息很重要可以设置为True进行彩色匹配但计算量更大且对颜色变化更敏感。3. 等待与容错策略UI渲染需要时间。不要假设截图后元素会立刻出现。# 不好的做法直接触摸可能因元素未加载而失败 touch(tpl_button) # 好的做法先等待元素出现设置超时时间 wait(tpl_button, timeout10) # 等待最多10秒 touch(tpl_button) # 更好的做法加入容错判断 if exists(tpl_button): touch(tpl_button) else: log(“按钮未找到执行备用操作”) # 可能先返回上一页或检查网络状态3.2 Poco的精准定位编写健壮的选择器当界面元素有控件树支持时Poco是更优的选择。它的脚本更稳定执行更快。1. 理解Poco的定位语法Poco的定位思想来源于前端支持多种选择器。from poco.drivers.android.uiautomation import AndroidUiautomationPoco poco AndroidUiautomationPoco() # 通过属性定位 poco(text“登录”).click() # 点击文本为“登录”的控件 poco(name“com.xxx:id/btn_login”).click() # 通过资源ID定位最稳定 poco(type“android.widget.Button”).click() # 点击第一个Button类型的控件不推荐易变 # 通过层级关系定位 poco(“com.xxx:id/parent_view”).child(“android.widget.Button”)[0].click() # 通过相对定位 poco(text“用户名”).parent().child(type“EditText”).set_text(“test”)2. 如何获取稳定的选择器使用AirtestIDE的Poco Inspector这是最佳途径。悬浮在控件上查看其属性。优先使用name通常是资源ID因为它在版本迭代中最稳定。避免使用绝对路径和索引像poco(“android.widget.FrameLayout”).child(“android.widget.LinearLayout”)[0].child(“android.widget.Button”)[2]这种定位方式极其脆弱UI结构稍有调整就会失败。利用多重属性组合如果单个属性不唯一可以组合使用。# 组合 text 和 type 属性进行精确定位 poco(text“确认”, type“android.widget.Button”).click()3. 处理动态内容和列表等待控件出现Poco同样需要等待。btn poco(name“dynamic_btn”).wait(10).click() # 等待最多10秒后点击遍历列表使用for循环遍历poco(“list_item”)获取的UI对象数组。处理滚动列表这是难点。通常需要结合swipe操作和exists判断。Airtest提供了poco.scroll()等方法但兼容性因平台而异可能需要自己实现一个“滚动直到找到元素”的通用函数。3.3 设备连接与初始化一切开始的地方稳定的设备连接是测试脚本能运行的前提。这里以最复杂的Android平台为例。1. ADB连接与端口管理Airtest底层通过ADB与Android设备通信。确保你的电脑已安装ADB并配置环境变量。# 检查设备是否连接 adb devices如果遇到设备离线、未授权等问题需要逐一排查开启USB调试在手机开发者选项里。授权电脑手机弹出授权框时要点允许。ADB版本冲突如果电脑上有多个ADB如Android Studio自带、其他工具自带可能会冲突。统一使用一个ADB版本并将其路径放在系统环境变量最前面。端口占用Airtest会尝试使用5037等端口如果被占用如其他手机助手需要关闭冲突进程。在脚本中建议使用auto_setup或显式连接from airtest.core.api import * # 自动连接当前唯一设备适用于单设备 auto_setup(__file__) # 或显式连接指定设备 connect_device(“Android:///” “设备序列号”) # 更稳定2. 初始化Poco连接设备后需要初始化对应平台的Poco实例。这一步必须在设备进入待测应用界面之后进行因为Poco需要获取当前应用的上下文。from poco.drivers.android.uiautomation import AndroidUiautomationPoco # 先确保设备已连接并进入应用 start_app(“com.xxx.your.app”) sleep(2) # 等待应用启动 # 然后初始化poco poco AndroidUiautomationPoco()对于Unity游戏则需要使用UnityPoco。4. 实操过程与核心环节实现让我们通过一个完整的实战案例将上述知识点串联起来。假设我们要测试一个简单的手机计算器应用Android实现打开应用依次输入12然后断言结果是否为3。4.1 环境准备与脚本结构首先确保Airtest环境已安装pip install airtest如果你需要用到Poco对原生Android的支持还需要安装对应驱动pip install pocoui # 通常安装airtest时会附带但最好确认一个良好的脚本结构应该包含初始化、测试用例、清理和报告生成。# test_calculator.py import sys import os sys.path.append(os.path.dirname(__file__)) from airtest.core.api import * from poco.drivers.android.uiautomation import AndroidUiautomationPoco import pytest # 可以使用pytest组织用例这里为简化直接用函数 def setup_module(): 全局初始化连接设备启动应用 global poco # 连接设备这里假设通过USB连接了一台设备 connect_device(“Android:///”) # 启动计算器应用包名需要替换为真实包名 stop_app(“com.android.calculator2”) # 先停止确保干净状态 start_app(“com.android.calculator2”) sleep(2) # 等待应用界面稳定 # 初始化Poco poco AndroidUiautomationPoco() def teardown_module(): 全局清理停止应用断开连接可选 stop_app(“com.android.calculator2”) # disconnect_device() # 通常不需要显式断开 def test_addition(): 测试加法功能 # 步骤1: 点击数字1 poco(name“com.android.calculator2:id/digit_1”).click() # 步骤2: 点击加号 poco(name“com.android.calculator2:id/op_add”).click() # 步骤3: 点击数字2 poco(name“com.android.calculator2:id/digit_2”).click() # 步骤4: 点击等号 poco(name“com.android.calculator2:id/eq”).click() # 步骤5: 断言结果 result poco(name“com.android.calculator2:id/result”).get_text() assert result “3”, f“加法结果错误期望3实际得到{result}” print(“加法测试通过”) if __name__ “__main__”: setup_module() try: test_addition() finally: teardown_module()4.2 混合使用图像识别与Poco假设这个计算器的“等于”按钮是一个自定义的、Poco无法识别的图片按钮。我们就需要混合使用技术。def test_addition_mixed(): 测试加法功能混合模式 # 使用Poco点击数字和运算符 poco(name“digit_1”).click() poco(name“op_add”).click() poco(name“digit_2”).click() # 使用图像识别点击“等于”按钮 # 假设我们已经有一张等于按钮的截图 “btn_equals.png” # 先等待按钮出现 wait(Template(r”btn_equals.png”, threshold0.8), timeout5) # 再进行点击 touch(Template(r”btn_equals.png”, threshold0.8)) # 断言结果依然使用Poco result poco(name“result”).get_text() assert result “3”这种混合模式在实际项目中非常常见它让你能灵活应对各种复杂的UI情况。4.3 生成与解读测试报告Airtest在运行脚本后会自动在项目目录下生成一个log文件夹里面包含一个HTML格式的测试报告。这个报告是排查问题的利器。如何生成报告最简单的方式是使用AirtestIDE运行它会自动生成。在命令行中可以使用airtest run命令并指定–log参数。airtest run test_calculator.py –device Android:/// –log ./logs报告里看什么测试概览通过/失败用例数总耗时。步骤详情报告会按时间线列出每一步操作touch,swipe,assert等并配有该步骤执行前后的屏幕截图。这是最核心的部分。错误信息如果某步失败会高亮显示并附上错误日志和堆栈信息。资源引用报告中会嵌入你脚本中使用的截图点击可以放大查看。实操心得养成每次运行后查看报告的习惯。特别是对于失败的用例通过对比失败步骤前后的截图你能快速判断是UI没加载出来等待时间不足、元素定位错了Poco选择器或截图问题还是出现了预期外的弹窗需要增加异常处理。这个报告也是向开发提Bug的有力证据。5. 常见问题与排查技巧实录即使理解了原理在实际项目中还是会遇到各种稀奇古怪的问题。下面是我总结的“踩坑大全”和应对策略。5.1 图像识别相关故障排查问题现象可能原因排查与解决思路识别不到/匹配失败1. 截图与屏幕实际内容差异大分辨率、主题、字体。2. 匹配阈值(threshold)设置过高。3. 屏幕动态变化加载动画、光标闪烁。4. 截图区域特征不明显。1.重新截图在目标设备相同状态下用AirtestIDE重新截取。2.降低阈值尝试0.6-0.7。3.增加等待在操作前加sleep或wait确保界面稳定。4.使用RGBTrue如果颜色是关键特征。5.尝试其他匹配算法Airtest支持BRISK、AKAZE等算法在复杂背景下可能更优但速度慢。识别到错误区域1. 截图区域在屏幕上多处出现如重复的图标。2. 阈值设置过低。1.扩大截图范围包含更多独特的上下文信息。2.提高阈值尝试0.8-0.9。3.使用target_pos即使匹配区域有偏差也能点击到正确子区域。4.结合Poco定位先定位到大致区域再在该区域内进行图像识别。识别速度慢1. 截图尺寸过大。2. 使用了复杂的算法如BRISK。3. 屏幕分辨率过高。1.优化截图尺寸。2.默认算法优先非必要不使用BRISK。3.降低屏幕分辨率在设备开发者选项或ADB命令中调整。adb shell wm size 720x1280一个高级技巧对于变化频繁的UI如数字、倒计时可以尝试使用“局部特征匹配”而非全局模板匹配。例如只截取数字“7”的特定拐角部分作为模板这样即使数字颜色、大小变化只要拐角形状相似就能匹配。这需要对图像特征有更深理解。5.2 Poco与设备连接问题问题现象可能原因排查与解决思路poco初始化失败或报错1. 设备未连接或未授权。2. 未进入正确的应用界面。3. 应用不是原生/游戏Poco驱动不对。4. Android系统版本过高UIAutomator服务问题。1.adb devices确认设备在线且授权。2. 确保start_app()后并sleep足够时间再初始化poco。3. 确认应用类型原生/RN/Flutter用AndroidUiautomationPocoUnity游戏用UnityPoco。4. 对于高版本Android尝试在开发者选项中关闭/打开“USB调试安全设置”或重启ADB服务。Poco找不到控件1. 控件属性在运行时变化如text动态加载。2. 控件在列表或滚动视图内未在当前视窗。3. 选择器写错了属性名或值不对。4. 页面是多层WebView或Flutter需要切换上下文。1. 使用wait等待控件出现或使用更稳定的属性如name(resource-id)。2. 先滚动到控件可见位置再定位。3. 用AirtestIDE的Poco Inspector实时查看控件属性核对选择器。4. 对于WebView可能需要poco AndroidUiautomationPoco(use_airtest_inputTrue, screenshot_each_actionFalse)并切换上下文。脚本在部分设备上运行失败1. 设备分辨率、DPI不同导致图像识别失败。2. 厂商定制ROM修改了控件属性或层级。3. 设备性能差响应慢。1.图像识别为不同分辨率准备多套截图模板或使用相对坐标。2.Poco定位避免使用绝对层级索引多用唯一的resource-id。3.增加等待时间在关键操作后加入sleep或使用poco.wait()。4.设备池管理对不同型号设备编写适配代码或配置文件。5.3 性能与稳定性提升技巧设置合理的等待与超时滥用sleep(固定时间)会导致脚本要么等太久慢要么等不够失败。优先使用wait和poco.wait()它们会在元素出现后立即返回并可以设置超时。截图压缩与缓存在脚本中频繁使用snapshot()或高分辨率截图会严重影响速度。Airtest默认会压缩截图。在非调试状态下可以考虑关闭不必要的截图如ST.SAVE_IMAGE False。使用try-except进行健壮性处理网络波动、临时弹窗都会导致脚本失败。在关键流程外包裹异常捕获实现优雅降级或重试。def safe_click(element, retry3): for i in range(retry): try: element.click() return True except TargetNotFoundError: sleep(1) log(“点击失败已达到最大重试次数”) return False独立测试用例减少耦合每个测试用例应该可以独立运行并处理好自己的前置进入某个页面和后置清理数据条件。避免用例之间严重的状态依赖。集成到CI/CD使用命令行工具airtest run结合 Jenkins、GitLab CI 等可以实现自动化测试的定时执行或触发执行。关键是要管理好测试设备如使用云测平台或公司内部的设备农场。5.4 一个真实的复杂场景处理应用权限弹窗这是一个非常典型的坑。很多应用在启动时会请求位置、存储等权限。这个弹窗的UI可能来自系统也可能来自应用自身且出现时机不确定。策略封装一个智能的“启动应用”函数。def smart_start_app(package_name): 智能启动应用处理可能的权限弹窗 start_app(package_name) sleep(3) # 等待应用启动 # 定义一系列可能出现的权限弹窗的“拒绝”按钮的定位方式图像或Poco permission_popups [ {“type”: “image”, “tpl”: “tpl_deny_button.png”, “action”: “touch”}, {“type”: “poco”, “selector”: “text‘拒绝’”, “action”: “click”}, {“type”: “poco”, “selector”: “text‘Deny’”, “action”: “click”}, # 可以添加更多语言的弹窗识别 ] for popup in permission_popups: for i in range(2): # 每个弹窗尝试两次 if popup[“type”] “image”: if exists(Template(popup[“tpl”], threshold0.8)): touch(Template(popup[“tpl”], threshold0.8)) sleep(1) elif popup[“type”] “poco”: try: poco(popup[“selector”]).click() sleep(1) except: pass # 最后再等待一下主界面稳定 sleep(2) # 使用方式 smart_start_app(“com.xxx.your.app”) poco AndroidUiautomationPoco() # 此时再初始化poco这个函数尝试识别并点击常见的权限拒绝按钮为后续测试扫清障碍。这体现了UI自动化测试中的一个重要思想脚本需要对环境有一定的“容错”和“自适应”能力。6. 进阶应用与框架扩展当你熟悉了Airtest的基本操作后可以考虑将其融入更专业的测试体系。6.1 与Pytest测试框架集成Airtest脚本本身是Python代码可以非常方便地集成到Pytest中利用Pytest强大的夹具fixture、参数化、钩子hook等功能来组织和管理用例。# conftest.py import pytest from airtest.core.api import * from poco.drivers.android.uiautomation import AndroidUiautomationPoco pytest.fixture(scope“module”) def device_setup(): 模块级别的设备初始化夹具 connect_device(“Android:///”) yield disconnect_device() pytest.fixture def app_setup(device_setup): 用例级别的应用初始化夹具 stop_app(“com.xxx.app”) start_app(“com.xxx.app”) sleep(2) poco AndroidUiautomationPoco() yield poco # 用例执行后可以回到主页或停止应用 stop_app(“com.xxx.app”) # test_cases.py class TestCalculator: def test_addition(self, app_setup): poco app_setup poco(name“digit_1”).click() poco(name“op_add”).click() poco(name“digit_2”).click() poco(name“eq”).click() assert poco(name“result”).get_text() “3” pytest.mark.parametrize(“a,b,expected”, [(1,2,3), (5,3,8)]) def test_addition_params(self, app_setup, a, b, expected): poco app_setup # 这里需要有一个能输入任意数字的计算器通过点击多个数字键实现 # 简化示例假设有数字键映射 click_digit(poco, a) poco(name“op_add”).click() click_digit(poco, b) poco(name“eq”).click() assert int(poco(name“result”).get_text()) expected这样你就可以用pytest test_cases.py -v –htmlreport.html来运行测试并生成更专业的测试报告了。6.2 搭建简单的自动化测试平台对于团队协作可以搭建一个轻量级的Web平台用来调度和执行Airtest脚本。后端Python Flask/Django提供API接口接收测试任务如脚本名、设备型号。后端服务使用subprocess模块调用airtest run命令行来执行对应的测试脚本。设备管理维护一个设备池通过ADB管理设备状态空闲/占用。任务队列使用Celery或Redis队列管理测试任务避免并发冲突。前端一个简单的Web界面用于上传脚本、选择设备、触发测试、查看历史报告。报告存储将每次运行的HTML报告和日志文件存储到服务器或对象存储如OSS并通过前端提供链接查看。这个平台的核心价值在于将Airtest的执行“服务化”让不熟悉命令行和环境的团队成员也能轻松触发自动化测试并集中管理测试资产和结果。6.3 测试脚本的维护策略UI自动化脚本最大的挑战是维护成本。应用UI一变脚本就可能大面积失败。页面对象模型Page Object Model, POM这是UI自动化测试的经典设计模式。将每个页面或重要组件封装成一个类页面的元素定位和基本操作作为类的方法。测试用例只调用这些方法不直接包含定位器。当UI变化时你只需要修改对应的Page类。class LoginPage: def __init__(self, poco): self.poco poco self.username_input “com.xxx:id/et_username” self.password_input “com.xxx:id/et_password” self.login_button “com.xxx:id/btn_login” def login(self, username, password): self.poco(self.username_input).set_text(username) self.poco(self.password_input).set_text(password) self.poco(self.login_button).click() # 在用例中 def test_login(self): login_page LoginPage(poco) login_page.login(“user”, “pass”) # ... 后续断言将定位器与代码分离可以将所有元素的定位信息如Poco选择器、截图路径提取到配置文件如YAML、JSON中。脚本读取配置文件来定位元素。这样UI变更时只需修改配置文件。定期执行与监控将自动化测试套件集成到每日构建Nightly Build中。一旦有脚本失败立即通知相关人员开发或测试检查是Bug还是脚本需要更新。这能保证脚本的“新鲜度”。Airtest作为一个强大的工具解决了UI自动化“能不能做”的问题。而如何将它用好构建起稳定、可维护的自动化测试体系则更需要工程化的思维和持续的努力。它不是一个“录制-回放”玩具而是一个需要认真设计和维护的代码项目。从我个人的经验来看在游戏测试、核心业务流程的冒烟测试、以及多设备兼容性测试上Airtest带来的效率提升是巨大的但前提是你需要花时间去理解它的原理并耐心地处理那些看似琐碎的细节问题。