夜雨聆风学习资料网

ARTICLE · 1021351

基于Harness工程优化APP自动化测试Skill实战案例

基于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
、页面名、控件类型
这个控件是谁
业务语义
控件名称、功能角色、动作意图
这个控件是干什么的
XML 信息
resource-id
、text、class、bounds
如何从结构树中找到它
层级上下文
父控件、兄弟控件、子控件
它和周围控件是什么关系
可交互属性
clickable、enabled、scrollable、editable
它能不能操作
视觉信息
截图区域、颜色、图标、相对位置
XML 无法表达什么
状态信息
页面进入前、动作后、页面变化
它出现在哪个状态
定位建议
ID、文本、组合条件、坐标候选
后续如何生成 locator
风险信息
稳定性、视觉缺口、待验证项
是否可以进入正式代码

一个控件画像大概可以抽象成这样:

{"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 同时负责页面探索、需求理解、代码生成、设备执行和问题修复。

职责混在一起之后,模型很容易出现两个问题:

  1. 上下文太长,Token 消耗明显增加;
  2. 前一个阶段的错误会被后一个阶段继续放大。

/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 定位
纯视觉定位
控件画像驱动定位
输入信息
UI XML
截图
XML + 截图 + 状态变化
是否依赖模型
通常不依赖
高度依赖
按需依赖
语义理解
弱
强
强
稳定性
XML 完整时较高
受分辨率和布局影响
综合多种证据
对 Compose 支持
一般
较好
较好
对动态列表支持
需要额外处理
容易误点
结合层级和上下文
定位可追溯性
较好
较弱
较强
Token 成本
低
高
中低
是否适合正式代码
适合
不建议直接使用
适合验证后固化
失败修复能力
依赖人工
容易重新猜
可以沿画像上下文修复

我自己的判断是:

  • 传统 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已放在测开学习圈里面啦,感兴趣的同学欢迎进圈获取,也可结合本文介绍自行实现

【往期推荐】

2026年最新AI测试开发学习路线发布了

我把资深QA设计用例思路蒸馏成了Skill,用例生成率直接起飞

AI测试流水线终于搭建完成了

一句话就能提Bug,这个Skill太好用了

从0到1做出可复用的 iOS 自动化测试 Skill,附真机演示效果

我做了一个APP自动化测试Skill,从此AI替你打工

大厂测开面试都在问AI,这个实战项目直接帮你准备好了

26届拿到字节测开实习offer(附面经)

你好,我是洋子,非典型理工男,毕业于中国传媒大学,前百度高级测试开发工程师,现AI测试工程师。利用半年时间,通过自学从功能测试进阶到测试开发,点击查看我的学习路线

我组建了测试开发学习圈子(限时特惠,立减51元),一直持续分享测开找工作的经验,提供AI测试/高频面试题答案/简历1V1优化/问题答疑/学习资料等服务,目前已经累计服务超过1000 人,欢迎点此了解

同时,我也是 B 站 UP 主:Bug挖掘机,日常分享软件测试领域干货知识,输出面试、工作经验,欢迎围观

AI测试开发学习交流群开放中,后台回复【进群】即可加入

关注公众号获取 原创测试开发学习路线/10G电子书/简历模板/大厂面试真题/测试工具/测试文档模板等福利

相关学习资料