字节跳动Airtest与Poco双框架融合实战:构建高稳定UI自动化测试体系 📅 2026/6/30 18:33:37 1. 项目概述字节跳动UI自动化测试框架的江湖地位在软件研发效能这个领域字节跳动一直是个“卷王”。当很多公司还在为如何落地UI自动化测试而头疼时字节内部已经孵化并开源了不止一套而是两套风格迥异、定位清晰的UI自动化测试框架。这背后反映的是超大规模、超快迭代的业务场景对测试基建提出的极致要求。今天我们不聊虚的就从一个一线测试开发工程师的视角来深度拆解这两款框架Airtest和Poco。很多人可能听说过它们但未必清楚它们的关系、差异以及字节跳动内部是如何将它们玩出花的。这不仅仅是两个工具的介绍更是理解现代互联网公司如何构建高可用、高效率自动化测试体系的一个绝佳样本。简单来说你可以把Airtest想象成一个“火眼金睛”的图像识别大师而Poco则是一个“庖丁解牛”的UI结构解析专家。它们一个偏“黑盒”一个偏“白盒”但组合起来却能应对从游戏、应用到小程序、H5等几乎全平台的UI自动化挑战。对于测试工程师、客户端开发甚至对质量保障感兴趣的产品经理来说理解这套组合拳能极大提升你对自动化测试的认知深度和实战能力。接下来我们就抛开官方文档那些标准话术用实战踩坑的经验带你看看这两款框架的核心门道。2. 框架核心定位与设计哲学拆解2.1 Airtest基于图像识别的跨平台UI自动化利器Airtest的核心设计哲学非常直接所见即所得以视觉为准绳。它不关心你的应用内部是什么控件树、什么渲染引擎它只关心最终呈现在屏幕上的像素点。通过计算机视觉CV技术Airtest能够识别、定位并操作屏幕上的特定图像区域。为什么字节跳动需要这样一个框架这得从它的业务多样性说起。字节的业务线涵盖手游如《航海王热血航线》、短视频抖音、资讯今日头条、办公套件飞书等等。尤其是游戏和部分重度依赖自研渲染引擎的App其UI元素可能无法通过传统的基于Accessibility或UI树解析的工具如Appium来定位。Airtest的图像识别能力恰好填补了这块空白。它就像一个“外挂”从用户视角模拟操作实现了真正的跨平台Windows, macOS, Android, iOS和跨引擎Unity, Cocos2d-x, 原生Android/iOS, 微信小程序等。核心技术点解析模板匹配Template Matching这是Airtest的看家本领。你只需要截取一张UI元素的图片作为“模板”Airtest会在当前屏幕画面中搜索相似度最高的区域。它内置了多种匹配算法如cv2.TM_CCOEFF_NORMED并允许设置阈值threshold默认0.8来控制匹配的严格程度。OCR文字识别集成了如PaddleOCR等引擎可以直接读取屏幕上的文字内容用于断言或逻辑判断。这在验证弹窗提示、读取列表项内容时非常有用。设备连接与屏幕控制提供了统一的API来连接和操控各种设备Android手机、iOS手机、Windows窗口等。对于Android它底层使用了adb对于Windows应用它可以直接获取窗口句柄进行操作。注意图像识别的稳定性是对比光源、屏幕分辨率、抗锯齿效果的“玄学”。在实际项目中模板图片的选取至关重要要尽量选择特征明显、背景简洁、不易受动态内容干扰的区域。2.2 Poco基于UI控件树的精准定位框架如果说Airtest是“大力出奇迹”的野路子那Poco就是“精准制导”的正规军。Poco的设计哲学是深入内部直接操控。它通过注入SDK或利用平台原生接口直接获取应用运行时的UI控件树UI Hierarchy然后像前端操作DOM一样通过选择器Selector来精准定位控件并触发其原生事件。为什么有了Airtest还需要Poco图像识别有其固有短板执行速度相对较慢需要截图、计算、受视觉变化影响大字体、主题、分辨率变化可能导致识别失败、无法获取控件非视觉属性如是否可点击、文本内容等。在追求稳定、快速且需要深度校验的自动化场景尤其是信息流App如抖音、今日头条的UI自动化中基于控件树的方式优势明显。Poco就是为解决这些问题而生。核心技术点解析多端SDK与统一协议Poco为不同平台提供了轻量级SDK如poco-sdkfor Android/iOS/Unity/Cocos等。这些SDK会在应用运行时将UI控件树信息通过一个统一的网络服务通常是WebSocket或HTTP暴露出来。测试脚本通过Poco库连接到这个服务即可远程“遥控”应用。灵活强大的选择器Poco提供了类似CSS或XPath的查询语法可以基于控件名name、类型type、文本text等多种属性进行定位还支持相对定位、正则表达式匹配等高级功能。例如poco(“com.bytedance.app:id/feed_list”).child(text“推荐”)。原生事件触发定位到控件后可以调用.click()、.swipe()、.set_text()等方法这些调用会通过SDK转化为平台原生的输入事件比图像识别的模拟点击更可靠、更快速。实操心得Poco的稳定性极大依赖于被测应用集成的SDK是否完善以及UI控件树是否规范。对于某些高度自定义的控件或游戏中的特效UIPoco可能无法识别此时就需要Airtest作为补充。两者是互补关系而非替代。3. 双框架融合实战架构设计与选型策略在实际的字节跳动内部项目中很少会单独使用Airtest或Poco而是根据测试场景的特点采用“Poco为主Airtest为辅”的混合模式。下面我们来拆解一个典型的融合架构是如何设计和选型的。3.1 混合框架的顶层设计一个健壮的UI自动化测试框架通常分为以下几个层次设备管理层负责设备的连接、状态监控、截图、日志收集。这一层通常由Airtest的device模块统一抽象因为它对多平台的支持最好。元素定位层这是核心决策层。脚本中会定义一个“定位器”Locator它内部封装了定位策略优先尝试使用Poco选择器定位如果失败超时或找不到元素则自动降级为使用Airtest图像识别进行定位。这种策略被称为“智能降级定位”。操作封装层将常用的业务操作如登录、发布、点赞、评论封装成独立的函数或Page Object。在这些封装里会调用定位层提供的统一接口进行操作。用例与数据层组织测试用例管理测试数据如账号、内容素材。报告与调度层生成包含详细步骤截图、操作日志的HTML报告并集成到CI/CD流水线中。为什么选择混合模式稳定性最大化Poco定位快且准是首选。对于Poco无法识别的“疑难杂症”如游戏技能图标、自定义绘制组件用Airtest兜底保证用例能继续执行。脚本可维护性对于稳定的UI控件使用Poco选择器定位即使UI样式微调只要控件属性不变脚本就无需修改。而纯图像脚本任何像素级变化都可能导致失败。执行效率Poco操作是原生事件速度远快于图像识别。混合模式确保了主体流程的高效执行。3.2 关键工具链与生态字节跳动围绕这两大框架构建了完整的工具生态这也是其能高效落地的关键AirtestIDE一个集成的开发环境提供了“所见即所得”的脚本录制、编辑、回放和调试功能。对于新手来说可以通过录制快速生成脚本是入门的神器。Poco Inspector通常内嵌在AirtestIDE中用于实时查看和选择应用内的UI控件树并生成对应的Poco选择器代码。这解决了手写选择器困难的问题。AirLab字节内部使用的云端真机测试平台。自动化脚本可以提交到AirLab在庞大的云端真机集群上并发执行并获取测试报告。这对需要大量兼容性测试的场景至关重要。选型决策流程图面对一个UI自动化需求你可以遵循以下决策路径开始 │ ├─ 是否游戏或强图形化应用 → 是 → 首选Airtest复杂逻辑结合Poco如果游戏接入了SDK │ ├─ 是否标准移动App/小程序/H5 → 是 → 首选Poco │ └─ 控件是否可通过SDK稳定获取 → 否 → 引入Airtest图像识别作为辅助定位 │ ↓ (是) 使用Poco进行主要自动化开发 │ ↓ 在Poco定位可能失败的环节如启动图、弹窗、验证码设置Airtest兜底 │ ↓ 结束4. 从零到一搭建混合自动化项目实战光说不练假把式。我们以一个简单的“抖音视频点赞”自动化场景为例演示如何搭建一个混合框架的测试脚本。假设我们要验证在抖音App中滑动推荐流并对第三个视频进行点赞操作。4.1 环境准备与初始化首先你需要安装核心库。通常使用Python作为脚本语言。# 安装Airtest核心库和Poco库 pip install airtest pip install pocoui # 注意是pocoui这是开源版本。字节内部可能有定制版。然后准备你的测试设备这里以Android手机为例并通过USB连接电脑开启ADB调试模式。在脚本开头我们需要初始化设备和Pocofrom airtest.core.api import * from poco.drivers.android.uiautomation import AndroidUiautomationPoco import time # 1. 连接设备自动查找已连接的设备 auto_setup(__file__) dev device() # 获取当前设备对象 # 2. 初始化Poco针对Android poco AndroidUiautomationPoco(use_airtest_inputTrue, screenshot_each_actionFalse) # use_airtest_inputTrue 让Poco使用Airtest的输入法兼容性更好 # screenshot_each_actionFalse 关闭每个动作都截图提升速度报告中可以手动控制截图点 # 3. 启动抖音假设包名已知 stop_app(“com.ss.android.ugc.aweme”) # 确保从干净状态开始 start_app(“com.ss.android.ugc.aweme”) time.sleep(5) # 等待App启动完成4.2 实现智能降级定位器这是混合框架的核心。我们编写一个通用的click_element函数。def click_element(poco_selector, template_image_pathNone, timeout10, threshold0.8): 智能点击元素。优先使用Poco失败后使用Airtest图像识别兜底。 :param poco_selector: Poco选择器对象例如 poco(“点赞按钮”) :param template_image_path: Airtest模板图片的路径 :param timeout: 定位超时时间 :param threshold: 图像匹配阈值 :return: True/False 表示是否点击成功 start_time time.time() # 策略1: 优先尝试Poco定位点击 while time.time() - start_time timeout: try: target poco_selector if target.exists(): target.click() print(f“通过Poco成功点击元素: {poco_selector}”) return True except Exception as e: # Poco定位可能出现的异常如元素不可点击等 print(f“Poco定位/点击尝试失败: {e}”) time.sleep(0.5) # 短暂等待后重试 # 策略2: Poco失败降级使用Airtest图像识别 if template_image_path: print(“Poco定位超时尝试使用Airtest图像识别...”) try: # 使用Airtest的touch接口基于图片模板进行点击 touch(Template(template_image_path, thresholdthreshold)) print(f“通过Airtest图像识别成功点击元素: {template_image_path}”) return True except TargetNotFoundError: print(f“Airtest也未找到图片模板: {template_image_path}”) return False else: print(“未提供图像模板无法使用Airtest兜底。”) return False4.3 编写业务测试用例现在我们利用上面的定位器来实现“滑动并点赞第三个视频”。def test_swipe_and_like_third_video(): 测试用例滑动推荐流点赞第三个视频 # 步骤1: 确保进入首页推荐流 # 这里假设首页的某个元素如“推荐”标签可以通过Poco定位 if poco(text“推荐”).exists(): print(“已进入推荐流页面”) else: # 如果Poco定位不到可以用Airtest点一下首页Tab的图片 touch(Template(“home_tab.png”)) time.sleep(2) # 步骤2: 向上滑动两次浏览到第三个视频附近 # 使用Poco的swipe接口更接近原生滑动 screen_width, screen_height poco.get_screen_size() start_point [screen_width * 0.5, screen_height * 0.7] end_point [screen_width * 0.5, screen_height * 0.3] poco.swipe(start_point, end_point, duration0.8) time.sleep(1.5) # 等待视频切换和加载 poco.swipe(start_point, end_point, duration0.8) time.sleep(1.5) # 步骤3: 定位并点击第三个视频的点赞按钮爱心图标 # 首先尝试用Poco定位抖音的点赞按钮通常有固定的resource-id或描述 # 我们需要通过Poco Inspector提前探查其属性 # 假设我们探查到点赞按钮的属性是desc“点赞” 或 name“like” like_button_poco poco(desc“点赞”) # 同时我们提前截取了一个点赞按钮的图片保存为 like_button.png like_button_image “like_button.png” # 使用智能定位器进行点击 click_success click_element( poco_selectorlike_button_poco, template_image_pathlike_button_image, timeout5 ) # 步骤4: 断言点赞成功 # 点赞成功后按钮状态通常会改变如变红desc变为“已点赞” if click_success: time.sleep(1) # 等待UI状态更新 # 断言1: 检查按钮状态是否改变Poco方式 if poco(desc“已点赞”).exists(): print(“断言成功点赞按钮状态已变为‘已点赞’。”) # 断言2: 或者检查是否有短暂的点赞动画或提示Airtest方式 # 可以尝试匹配点赞后的特效图片 try: assert_exists(Template(“like_success_toast.png”), “点赞成功提示未出现”) print(“断言成功检测到点赞成功视觉反馈。”) except AssertionError as e: print(f“视觉反馈断言失败但可能UI设计如此: {e}”) print(“测试用例执行通过”) else: print(“测试用例失败未能成功点击点赞按钮。”) raise AssertionError(“点赞操作失败”) # 执行测试用例 if __name__ “__main__”: try: test_swipe_and_like_third_video() finally: # 测试结束后可以回到首页或停止App keyevent(“HOME”) stop_app(“com.ss.android.ugc.aweme”)这个例子展示了如何将Poco的精准定位与Airtest的图像兜底结合起来并加入了基本的断言逻辑。在实际项目中你会将click_element这类通用函数、设备初始化、Page Object页面对象等封装成独立的模块或类使测试脚本更清晰、更易维护。5. 深度踩坑实录与性能优化指南用了这么久坑肯定没少踩。下面分享几个最具代表性的问题及其解决方案这些都是官方文档里不会细说的“血泪经验”。5.1 稳定性陷阱图像识别飘忽不定问题现象同一张模板图片在分辨率不同的手机上有时能匹配到有时匹配不到。或者在应用UI微调如按钮颜色深浅变化后脚本大面积失败。根因分析分辨率与缩放Airtest的模板匹配是基于像素的。不同分辨率的屏幕UI元素的绝对像素尺寸不同。抗锯齿与阴影UI渲染时的抗锯齿效果、细微的阴影或光效会导致元素边缘像素与截图模板有差异。动态内容例如按钮上有一个未读消息红点数字会变这会导致模板特征发生根本变化。解决方案与优化技巧黄金法则制作“鲁棒性”模板截图时尽量包含元素的独特纹理或形状特征而不是纯色块。例如一个图标比一个矩形色块更好。使用AirtestIDE提供的裁剪和边缘检测功能只保留最具特征的部分。适当降低匹配阈值threshold比如从0.8调到0.7以容忍细微变化。但要注意平衡太低会导致误匹配。多分辨率适配策略方案一推荐使用resolution关键字进行等比例缩放匹配。Airtest支持在初始化时指定一个基准分辨率脚本运行时会自动进行缩放计算。但这需要对所有模板图片进行统一管理。方案二为不同分辨率的主流机型维护不同的模板图片库。可以通过dev.get_current_resolution()获取当前设备分辨率然后动态加载对应的模板路径。应对动态UI对于红点数字可以截取不包含数字部分的图标区域作为模板。或者使用Poco来定位这类元素如果红点本身是一个控件。5.2 Poco控件识别“失灵”的排查思路问题现象在Poco Inspector里能看到控件但脚本运行时poco(selector).exists()返回False或者点击无效。根因分析SDK未正确集成或初始化这是最常见的原因。被测应用没有集成Poco SDK或者集成了但未在测试时正确初始化如Unity游戏需要在特定场景初始化Poco。控件属性动态变化控件的name、text等属性是动态生成的每次运行都不同。非标准控件/原生渲染一些完全自定义绘制的控件或使用Flutter等框架中某些特殊组件Poco SDK可能无法捕获其信息。多进程/多窗口应用有多个进程如某些游戏或者有悬浮窗、弹窗Poco可能连接到了错误的UI线程。排查与解决步骤确认SDK状态首先检查Poco Inspector是否能正常连接并显示控件树。如果不能问题出在环境或SDK集成上。使用更稳定的定位属性优先使用resource-idAndroid或accessibility-idiOS这些通常是开发静态定义的最稳定。避免使用text、desc等可能随内容变化的属性作为唯一标识可以结合其他属性如type一起使用。使用正则表达式进行模糊匹配例如poco(textMatches“.*登录.*”)。层级定位与等待控件可能还没加载出来。使用poco(selector).wait_for_appearance(timeout)代替exists()。使用相对定位或层级定位来缩小范围提高准确性。例如poco(“MainPanel”).child(“LoginButton”)。备用方案对于始终无法被Poco识别的“钉子户”控件果断在脚本中为其配置Airtest图像模板使用混合定位器。5.3 执行效率优化让脚本快起来UI自动化脚本慢是影响CI/CD流水线反馈速度的瓶颈。优化点主要在两个层面1. 操作等待策略优化减少固定等待time.sleep这是万恶之源。用显式等待wait_for_appearance或轮询检查替代固定的sleep。设置合理的超时时间根据网络状况和设备性能为不同的操作设置不同的超时。首页加载可能需5秒一个点击反馈可能只需1秒。并行与异步对于可以并行的操作如初始化多个模块考虑使用多线程。但设备操作本身通常是串行的所以主要优化点在逻辑等待上。2. 截图与报告优化按需截图不要每个步骤都截图。只在关键检查点、失败时或操作前后进行截图。可以通过封装的方法控制例如log(“开始登录”, snapshotTrue)。压缩截图Airtest生成的PNG截图较大可以在保存时进行压缩或集成到报告时再压缩。使用轻量级报告Airtest默认的HTML报告很详细但较重。对于大量用例可以考虑使用更简洁的报告格式或只对失败用例生成详细报告。3. 设备与脚本管理优化设备池预热在CI/CD中保持设备池处于就绪状态App已安装ADB已连接避免每个任务都从头开始连接安装。脚本模块化与数据驱动将通用操作封装避免代码重复。使用数据驱动如CSV、JSON管理测试数据和用例使脚本逻辑更清晰也便于维护。6. 集成与进阶在CI/CD流水线中落地个人跑通脚本只是第一步真正的价值在于集成到团队的持续集成/持续部署CI/CD流水线中实现无人值守的自动化测试。这里分享字节跳动内部实践的一些关键点。6.1 流水线集成模式通常采用“任务队列设备农场”的模式。触发代码合并请求Merge Request或每日定时构建触发自动化测试任务。调度CI系统如Jenkins, GitLab CI, 或字节内部的将任务放入队列并请求设备管理平台如AirLab分配一台或多台符合要求的空闲设备如指定型号、系统版本的手机。执行设备平台在指定设备上执行测试脚本通常通过airtest run命令行运行。脚本会从代码库拉取测试数据可能来自独立的资源服务器。收集脚本执行过程中产生的日志、截图、性能数据如有被实时收集并上传到存储服务。报告任务结束后生成一份聚合的测试报告HTML格式并通过邮件、IM机器人如飞书、钉钉通知相关人员。报告会高亮显示失败的用例和步骤并附上错误日志和截图。设备回收测试完成设备被重置或清理归还到设备池准备执行下一个任务。6.2 关键配置与脚本改造为了让脚本能在无GUI的CI环境中稳定运行你需要对本地调试脚本做一些改造使用命令行运行放弃AirtestIDE使用airtest run命令。airtest run test_case.air --device Android:///手机序列号 --log logs/ --report report.html无头模式与多设备对于Android可以配合adbover WiFi或USB Hub管理多设备。确保脚本能通过设备序列号动态连接设备而不是写死。错误处理与重试机制在CI中一次性的网络抖动或设备临时卡顿都可能导致用例失败。需要在脚本中增加关键步骤的自动重试机制并对最终失败进行准确断言避免误报。测试数据外部化账号、密码、测试内容等敏感或经常变动的数据绝不能硬编码在脚本里。应该从环境变量、配置文件或安全的存储服务中读取。6.3 效果评估与维护上线自动化不是终点而是起点。需要建立度量指标来评估其健康度和价值通过率/稳定性核心指标。如果通过率持续低于某个阈值如95%就需要排查是脚本问题、环境问题还是应用变更问题。执行耗时监控单用例和整套用例集的平均执行时间作为优化和CI流水线时长的依据。缺陷发现率自动化测试发现了多少手工测试遗漏的Bug这是其价值的直接体现。维护成本每周需要花多少时间来修复因UI变更而失败的自动化脚本这个成本需要被控制。维护方面建议成立一个虚拟的“自动化脚本维护小组”定期如每周查看失败用例区分是“脚本过时”还是“真实Bug”。对于“脚本过时”要及时更新定位器或图像模板。同时与开发团队建立良好的沟通机制当UI有重大重构计划时能提前通知测试团队以便评估自动化脚本的影响范围。我个人在推动这套框架落地的过程中最大的体会是技术选型没有银弹Airtest和Poco的组合提供了足够的灵活性和兜底能力但最终的成功取决于团队是否建立了与之匹配的工程化实践和维护文化。工具本身解决了“能不能做”的问题而流程和规范决定了“能不能做好、能不能持续”。从一两个冒烟测试用例开始逐步扩展覆盖范围持续优化稳定性让自动化真正成为研发流程中可信赖的一环这才是我们追求的目标。