ARTICLE · 1021351
基于Harness工程优化APP自动化测试Skill实战案例
大家好,我是洋子,用 AI 做 APP 自动化,很多人最先想到的是,让 AI 看一眼页面,然后告诉它:
“帮我点击通讯录,再搜索一个好友,最后发送消息。”
听起来很简单。但真正跑起来之后,问题通常会一个接一个出现:
AI 找到了相似的控件,但点击了错误的按钮; 页面上明明有这个文字,XML 里却找不到; 同一个控件,在不同页面、不同状态下,定位方式完全不一样; 页面滚动、弹窗出现、键盘弹起之后,原来的坐标全部失效; 测试代码生成出来了,但到底为什么这样定位,没有任何可追溯信息。
以前我们经常把这些问题归结为:
“AI 还不够聪明。”
但后来我发现,依据Harness工程的概念,很多时候并不是 AI 不聪明,根因是我们交给 AI 的输入太粗糙,导致模型上下文缺失或理解不到位。
我们只给了它一棵 UI XML 树(基于UiAutomator生成的APP页面无障碍树),或者一张截图,却没有告诉它:
这个控件在页面中的业务角色是什么; 它和其他控件之间是什么关系; 它出现在哪个状态; 点击它之后页面发生了什么变化; XML 信息和视觉信息之间是否一致; 这个控件是否真的适合被自动化定位。
所以我最近在 APP 自动化链路里,引入了一层新的能力:
先生成控件画像,再让 AI 基于画像生成测试用例和定位器。
这套能力不是简单地“让大模型看截图”,而是把 UI XML、页面截图、控件上下文、动作前后变化和定位风险统一整理成一份可消费的画像数据。
并且,我把它封装成了一个由 Skill 驱动的自动化流程。
unsetunset一、为什么单靠 UI XML 做自动化不够?unsetunset
在 Android 自动化中,UiAutomator XML 一直是非常重要的基础数据。
它可以告诉我们:
控件的 resource-id控件的 text控件的 class是否可点击 是否可输入 是否可滚动 控件所在的 bounds 控件的父子层级
对于很多传统页面来说,这些信息已经足够生成定位器。
例如:
<android.widget.Buttonresource-id="com.example:id/login_button"text="登录"clickable="true"bounds="[120,820][960,920]" />测试代码可以直接写成:
driver.find_element( AppiumBy.ID,"com.example:id/login_button").click()但现在的 APP 页面越来越复杂,尤其是大量使用 Jetpack Compose、Flutter、WebView、动态列表和自绘控件之后,单纯依赖 XML 会出现几个问题。
1. XML 里有控件,但没有业务语义
XML 可能只告诉你这是一个:
android.view.View但它没有告诉你这个 View 是:
“添加好友” “打开扫一扫” “复制个人 ID” “发送消息” “展开更多操作”
对于人来说,看截图可能一眼就能判断。
但对于自动化代码生成来说,class、text 和 bounds 并不一定足够。
2. XML 里的文字不等于真正的操作目标
例如一个列表项可能包含:
好友名称在线状态最后一条消息未读数量更多按钮如果只按照文字搜索,AI 可能找到的是列表项,而不是列表项右侧的“更多”按钮。
它知道“我要操作这个好友”,但不知道:
真正可点击的目标,是这个好友条目内部的哪个子控件?
3. 页面状态变化会让定位失效
同一个页面,在不同状态下可能完全不一样:
初始状态 输入关键字后 搜索结果返回后 键盘弹起后 弹窗出现后 页面滚动后 网络加载完成后
如果只保存一份静态 XML,AI 很难判断:
这个控件是页面固有元素,还是某次操作之后临时出现的元素?
4. 坐标能点击,不代表坐标可靠
视觉模型经常可以给出一个看起来非常合理的坐标。
例如:
{"action": "tap","x": 864,"y": 1532}这次点击可能确实成功。
但屏幕分辨率、字体大小、系统导航栏、键盘状态一变化,这个坐标就可能失效。
所以坐标应该被当作:
视觉层的候选证据,而不是正式定位器。
unsetunset二、我的方案:先做控件画像,再生成自动化代码unsetunset
我现在采用的整体思路是:
页面遍历 / UI Dump ↓采集 XML + 截图 + 页面状态 ↓离线解析控件结构 ↓生成控件画像 ↓检测 XML 无法表达的视觉缺口 ↓必要时调用视觉模型补充语义 ↓生成测试意图与用例矩阵 ↓基于画像生成 locator ↓设备验证点击与页面指纹 ↓固化为正式自动化代码这里面最重要的变化是:
以前的链路是:
UI XML → AI 直接生成脚本现在变成:
UI XML + 截图 + 动作上下文 ↓控件画像 ↓测试用例 ↓定位器 ↓正式脚本这相当于在 AI 和原始页面之间增加了一层“结构化中间表示”。
这个中间表示,就是控件画像。
unsetunset三、什么是 APP 自动化里的控件画像?unsetunset
简单说,控件画像不是一份简单的控件列表,而是对页面上关键控件的结构化描述。
它通常会包含下面几类信息:
control_id | ||
resource-id | ||
一个控件画像大概可以抽象成这样:
{"control_id": "add_friend.search_button","role": "搜索好友入口","page": "add_friend","xml": {"class": "android.widget.EditText","text": "搜索","clickable": true,"enabled": true,"bounds": "[96,240][984,338]" },"context": {"parent": "add_friend.search_container","siblings": ["add_friend.scan_button","add_friend.contact_button" ] },"actions": ["tap","input_text" ],"locator_candidates": ["resource-id","text + class","parent-child relationship" ],"risk": "low","verification": "pending"}注意,这里不是让视觉模型直接输出一段 Appium 代码。
而是先让它回答一个更基础的问题:
这个页面上有哪些控件?每个控件的业务角色是什么?哪些信息来自 XML,哪些信息必须依赖视觉判断?
unsetunset四、控件画像的核心价值,不只是“识别控件”unsetunset
很多人看到“控件画像”,第一反应是:
这不就是把控件信息整理成 JSON 吗?
如果只是整理 JSON,确实价值有限。
控件画像真正重要的地方,是它把“页面理解”从一次性的模型输出,变成了一份可以被多个阶段复用的自动化资产。
1. 让 AI 从“猜控件”变成“查控件”
没有画像时,AI 需要自己从 XML 和截图中临时判断:
我觉得右上角这个图标应该是搜索按钮有画像后,AI 可以直接查询:
control_id: add_friend.search_buttonrole: 搜索好友入口state: defaultlocator: resource-id 优先risk: low这两种方式的差异很大。
前者是临时推理。
后者是基于已确认资产做决策。
2. 让测试意图和定位器解耦
测试用例阶段不应该直接写死 XPath、坐标和 Page Object。
例如测试意图可以写成:
{"case_id": "ADD-FRIEND-001","title": "通过搜索入口添加好友","steps": [ {"action": "tap","target": "搜索好友入口" }, {"action": "input","target": "好友搜索输入框","value": "${friend_name}" } ]}此时它描述的是:
我要完成什么业务动作。
而不是:
我要点击哪个 XPath。
真正的 locator 由后续 Skill 根据画像、UI 规则和设备验证结果生成。
这样做的好处是:
页面结构变化时,不需要修改所有测试意图; 同一个业务动作可以适配多个页面版本; 测试用例更适合人工审核; 定位器可以由专门阶段统一治理。
3. 让失败问题更容易分析
如果测试失败了,传统日志可能只告诉你:
NoSuchElementException但画像系统可以进一步告诉你:
目标控件:add_friend.search_button失败阶段:搜索结果页原定位方式:resource-id当前页面:add_friend_search_result页面指纹:发生变化风险判断:控件可能被列表结果覆盖建议动作:回退到父容器 + 文本组合定位这对自动修复非常重要。
AI 不再只是看到“找不到元素”,而是能知道:
找的是哪个业务控件; 当时页面处于什么状态; 之前使用了什么定位策略; 页面发生了什么变化; 应该优先尝试哪种修复方式。
unsetunset五、我的 Skill 是怎么工作的?unsetunset
我把这条链路拆成了几个相对独立的阶段。
/dump-ui ├── 遍历页面 ├── 采集 UI XML ├── 截取页面截图 ├── 记录页面指纹 └── 生成控件画像/gen-tests ├── 读取需求和测试意图 ├── 复核控件画像 ├── 生成测试矩阵 ├── 生成 locator └── 输出 Appium pytest 用例/run-and-fix ├── 指定设备执行 ├── 收集失败证据 ├── 判断定位问题或产品问题 ├── 修复定位器与等待逻辑 └── 重新执行并生成报告这里有一个原则非常重要:
画像阶段负责理解控件,生成阶段负责设计用例,执行阶段负责验证结果。
不能让一个 Skill 同时负责页面探索、需求理解、代码生成、设备执行和问题修复。
职责混在一起之后,模型很容易出现两个问题:
上下文太长,Token 消耗明显增加; 前一个阶段的错误会被后一个阶段继续放大。
/dump-ui 与/gen-tests 的两个skill 工作流程图如下: 
unsetunset六、控件画像的生成,不是“一上来就调用大模型”unsetunset
这一点可能是整个方案里最容易被忽略的地方。
如果每个页面、每个控件、每次运行都直接调用视觉模型,成本仍然会很高,而且结果未必稳定。
所以我的做法是:
先离线规则解析,发现视觉缺口之后,再按需调用多模态模型。
第一层:离线解析 XML
先通过确定性规则提取:
可点击控件; 可输入控件; 可滚动控件; 有文本的控件; 有 resource-id 的控件; 父子关系; 兄弟关系; 页面边界; 控件层级; 可能的重复控件。
这一步不需要消耗模型 Token。
而且它的结果可重复、可审计。
第二层:自动判断视觉缺口
下面几类情况,可以认为 XML 存在视觉缺口:
控件没有文本; 控件没有 resource-id; 多个控件属性完全相同; 页面使用自绘图标; 控件的视觉意义依赖图标; XML 的 clickable 节点和截图中的可操作区域不一致; 动作前后出现了无法解释的页面变化; 搜索结果、弹窗和 Compose 组件无法通过结构直接区分。
第三层:只让视觉模型补充缺失信息
视觉模型重点补充:
控件在截图中的视觉角色; 图标对应的业务功能; 控件之间的相对关系; 哪个区域才是真正可点击区域; 动作前后发生了什么; 是否存在遮挡、弹窗、键盘或滚动导致的状态变化。
但视觉模型不会直接覆盖最终 locator。
它输出的是:
视觉候选语义而不是:
正式自动化定位器unsetunset七、为什么不让视觉模型直接生成定位器?unsetunset
因为视觉模型看到的是“当前画面”,而自动化定位器面对的是“未来的页面”。
这两者不是一回事。
比如视觉模型判断:
右上角这个图标应该是扫一扫按钮这个判断可能是对的。但它不应该直接把:
driver.tap([(934, 148)])写入正式代码。正确的流程应该是:
视觉判断:右上角图标是扫一扫 ↓画像登记:add_friend.scan_button ↓检查 XML 是否存在对应控件 ↓生成 resource-id / content-desc / 层级组合候选 ↓在指定设备上点击验证 ↓确认页面指纹变化 ↓通过后固化 locator换句话说:
视觉模型负责提出假设,设备执行负责验证假设。
这是一个非常关键的边界。
unsetunset八、传统定位方式和控件画像方案对比unsetunset
我自己的判断是:
传统 XML 定位适合结构稳定、属性完整的页面; 纯视觉定位适合探索和补充,但不适合作为唯一方案; 控件画像定位更适合复杂 APP 的长期自动化资产建设。
它不是要替代 XML,也不是要让视觉模型接管一切。而是把两类信息放到同一个决策链路里。
unsetunset九、实践案例:调用Skill 生成【添加好友】模块 APP自动化测试用例unsetunset
下面以一个比较典型的“添加好友”流程为例。
业务流程是:
进入联系人页面 ↓点击添加好友 ↓选择搜索好友 ↓输入好友 ID ↓点击搜索 ↓进入搜索结果 ↓点击添加 ↓发送验证消息我这里以Codex 调用Skill为例子,先把Skill 放在Codex 能自动发现加载Skills的目录下,路径为一般为~/.codex/skills
第一步,调用Dump-UI Skill获得APP内页面结构和控件画像
可以看到调用Dump-UI Skill后,在control-profiles目录下的各页面控件画像文件已生成,以功能命名为JSON格式
第二步,调用prd-parse Skill 解析需求文档,以添加好友模块为例

第三步,调用gen-test skill 生成APP自动化测试用例 
第四步,调用run-and-fix skill 进行真机运行以及失败自愈

本文提到的Skill已放在测开学习圈里面啦,感兴趣的同学欢迎进圈获取,也可结合本文介绍自行实现
我把资深QA设计用例思路蒸馏成了Skill,用例生成率直接起飞
从0到1做出可复用的 iOS 自动化测试 Skill,附真机演示效果
我做了一个APP自动化测试Skill,从此AI替你打工
大厂测开面试都在问AI,这个实战项目直接帮你准备好了
26届拿到字节测开实习offer(附面经)

你好,我是洋子,非典型理工男,毕业于中国传媒大学,前百度高级测试开发工程师,现AI测试工程师。利用半年时间,通过自学从功能测试进阶到测试开发,点击查看我的学习路线
我组建了测试开发学习圈子(限时特惠,立减51元),一直持续分享测开找工作的经验,提供AI测试/高频面试题答案/简历1V1优化/问题答疑/学习资料等服务,目前已经累计服务超过1000 人,欢迎点此了解
同时,我也是 B 站 UP 主:Bug挖掘机,日常分享软件测试领域干货知识,输出面试、工作经验,欢迎围观
AI测试开发学习交流群开放中,后台回复【进群】即可加入
关注公众号获取 原创测试开发学习路线/10G电子书/简历模板/大厂面试真题/测试工具/测试文档模板等福利