移动端自动化测试为什么越做越痛?大模型如何破局?
文章目录
之前写到过 AI 的代码可以通过形式化验证来保证安全性和正确性,但是对于移动端还是得通过实际测试,所以好奇 AI 给移动端测试带来的发展,本文来了解一下移动端的自动化测试。
从 Monkey 随机乱点到 LLM 驱动的智能体自主探索——移动端 GUI 测试正经历一场范式转移。这篇文章先从传统方法的三大瓶颈讲起,再拆解 GPTDroid、DroidBot-GPT、DroidAgent 三个开创性工作,看看大语言模型是如何第一次"拿起手机"测 App 的。
注:本文有 AI 辅助创作,确实排版和阅读都比纯手工更好。
1、先说结论
移动端 UI 自动化测试正在从"定位控件并点击"转向"理解语义并推理"。这不是一次增量改进,而是一次范式转移。
本文的核心观点:
- 传统 UI 测试的三大瓶颈(脆弱定位、跨端鸿沟、Oracle 缺失)都源于同一个根因:机器不理解页面上写的是什么。
- LLM 天然具备三种传统测试工具缺失的能力:理解自然语言语义、生成有意义的文本输入、进行多步推理。
- 2023-2024 年的三个开创性工作(GPTDroid、DroidBot-GPT、DroidAgent)已经验证了这条路径的可行性,并将 Activity 覆盖率提升了 32%。
全文约 7500 字,适合有一定客户端或测试工程背景的读者。
2、传统 UI 测试为什么"越做越痛"?
如果你做过客户端开发或 QA,大概率经历过以下场景:
2.1、痛点一:一次 UI 改版,百条脚本阵亡
你基于 Espresso 或 XCUITest 写了 200 条回归测试。设计师改了首页布局——搜索栏从顶部移到了浮层弹窗里——于是所有涉及"搜索"的测试用例集体红屏。
问题的根源在于:传统测试脚本通过控件 ID(Resource ID) 或 XPath 定位页面元素。这类定位器高度依赖 UI 的层级结构,稍有变化就会断裂。
|
|
如果 home_search_bar 这个 ID 在新版本中被重命名或层级发生变化,这条测试就废了。工程实践中常见的 Page Object 模式 能缓解耦合——把页面元素封装为对象——但本质上仍然是"定位控件 → 操作控件"的脚本范式。UI 结构的根本性重构面前,Page Object 也无能为力。
2.2、痛点二:Android 写一遍,iOS 再来一遍
你的 App 有 Android 和 iOS 两个版本,业务逻辑完全相同,但需要维护两套独立的测试代码:
| 平台 | 测试框架 | 定位方式 | 断言 API |
|---|---|---|---|
| Android | Espresso / UIAutomator | withId() / withText() |
check(matches(...)) |
| iOS | XCUITest | buttons["Login"] / AccessibilityIdentifier |
XCTAssert(...) |
如果再加上 Flutter(Skia 自渲染,完全绕过原生视图树)、小程序(另一套 DOM 结构)、鸿蒙(ArkUI),维护成本是平台数量的乘法而非加法。
更致命的是,同一个功能在不同平台的 UI 实现可能不同。比如 Android 上"搜索"是首页顶部一个可直接输入的文本框,iOS 上是一个搜索图标,点击后跳转到独立的搜索页——操作步骤都不一样,控件映射根本映不上。
2.3、痛点三:只能查"心跳",不能做"体检"
传统自动化测试的判定标准(术语叫 Test Oracle)几乎只有一条:App 有没有崩溃(Crash)。
但 Crash 只是冰山一角。真正影响用户体验的缺陷往往是非崩溃功能性 Bug:
- 搜索"iPhone",结果全是 Android 手机壳——搜索逻辑错了,但 App 没崩溃。
- 使用"满 100 减 20"优惠券后,总价没变——支付逻辑错了,但 App 没崩溃。
- 切换到英语,半数界面仍是中文——国际化不全,但 App 没崩溃。
- 商品详情页的图片加载失败,显示灰色占位图——数据异常,但 App 没崩溃。
这些功能性缺陷,传统的 Monkey、Stoat、Sapienz 一个都发现不了。因为它们压根不知道"搜索 iPhone 应该出现 iPhone 商品"——它们不理解业务语义。
2.4、三个痛点的共同根因
仔细看这三个痛点:
- 脆弱定位 → 不理解"搜索栏搬到浮层里了,但功能没变"
- 跨端鸿沟 → 不理解"Android 和 iOS 的搜索入口不同,但业务意图相同"
- Oracle 缺失 → 不理解"搜索 iPhone 应该返回 iPhone 商品"
它们都指向同一个根因:传统测试工具不理解自然语言语义。
这正是大语言模型(LLM)最擅长的领域。
3、传统方法是怎么走到瓶颈的
在进入 LLM 之前,有必要快速回顾传统 GUI 测试的三个阶段,理解为什么走到了瓶颈。
3.1、阶段一:随机模糊测试(2008-至今仍在用)
代表工具:Android Monkey。
原理极其简单——向 App 注入随机的触摸、滑动、按键事件。优势是零配置、即开即用,适合冒烟测试。但问题也很明显:
- 覆盖率极低:随机点击走不到深层功能。如果某个功能需要"先登录 → 再搜索 → 再加购物车 → 再结算"四步操作,随机策略命中这个序列的概率约等于零。
- 无法生成有意义的输入:注册页面需要填手机号,Monkey 只会输入乱码。
- 发现的 Bug 无法复现:随机序列难以确定性重放。
3.2、阶段二:基于模型的系统遍历(2016-2022)
代表工作:Stoat[4]、Sapienz[5]。
思路是用状态转移图(State Transition Graph, STG) 建模 App 的 GUI 状态空间——每个页面是一个节点,每次操作是一条边——然后用 DFS/BFS 或进化算法系统遍历。
|
|
Stoat 用两阶段策略(先随机探索建模,再基于模型引导遍历),显著提升了覆盖率。Sapienz 更进一步,将覆盖率最大化建模为多目标优化问题。
但这一代方法有两个致命限制:
- 状态爆炸:复杂 App 的 GUI 状态可达数千个,遍历时间呈指数增长。
- 文本输入黑洞:DFS/BFS 碰到文本框就卡住了——“请输入搜索关键词”,遍历算法不知道该输什么。研究表明,真实 App 中约 30% 的功能分支隐藏在文本输入之后[1]。这意味着将近三分之一的功能,遍历算法压根触碰不到。
3.3、阶段三:手写脚本 + Page Object(工业界主流)
这是当前工业界的主流实践:QA 工程师手动编写测试脚本,通过 Page Object 模式管理页面元素。
|
|
Page Object 让脚本的可维护性好了很多——UI 变了只需改 Page Object 类,不用改测试逻辑。但它的本质仍然是:人工编写 → 人工维护 → 平台绑定。当你有 500 条测试用例、4 个平台要支持、每两周一次版本迭代时,维护成本就是一笔巨大的"技术债"。
3.4、小结:传统方法的演进逻辑
|
|
三个阶段的演进逻辑是"让机器更聪明地点击",但始终没能跨越 “理解用户意图” 的鸿沟。2023 年,LLM 的出现为这个鸿沟提供了桥梁。
4、LLM 第一次"拿起手机"
4.1、一个关键洞见
2023 年,多个研究团队几乎同时意识到了一件事:
LLM 天然具备三种传统测试工具缺失的能力:
- 理解自然语言——能"读懂"页面上的文字和按钮含义
- 生成有意义的文本——知道搜索框里应该输"运动鞋"而不是乱码
- 多步推理——能规划"先登录 → 再搜索 → 再加购物车"的操作路径
如果把 App 当前页面的信息"翻译"成自然语言告诉 LLM,然后让 LLM 决定"下一步做什么"——这不就是一个自动化的测试工程师吗?
4.2、从 GUI 树到 Prompt:那个"翻译"是怎么做的?
上一节说"把页面信息翻译成自然语言",这句话听起来理所当然,但具体怎么翻译,直接决定了 LLM 能不能"看懂"屏幕。
这条管线分三步:获取 → 过滤 → 序列化。
4.2.1、第一步:从手机获取原始 GUI 树
Android 上的标准做法是通过 UIAutomator 导出当前屏幕的视图层次结构:
|
|
导出的是一棵 XML 树,每个节点对应屏幕上一个控件。以一个购物 App 的搜索页为例(简化后):
|
|
每个 <node> 的属性中,对 LLM 有价值的信息分布并不均匀:
| 属性 | 含义 | 对 LLM 的价值 |
|---|---|---|
text |
控件显示的文字 | 最高——LLM 理解语义的核心依据 |
content-desc |
无障碍描述(给屏幕阅读器准备的) | 高——图标类控件的语义全靠它 |
class |
控件类型(Button / EditText / TextView) | 高——区分"按钮"和"输入框" |
clickable / scrollable |
是否可交互 | 高——知道哪些控件能操作 |
resource-id |
开发者定义的 ID | 中——有时有语义(search_bar),有时是乱码 |
bounds |
屏幕坐标 [左,上][右,下] |
用于执行——告诉系统"点哪里" |
iOS 上等价物是 XCUITest 的 Accessibility Snapshot,数据结构类似但格式不同(JSON 而非 XML)。
4.2.2、第二步:过滤噪声节点
一个中等复杂页面的原始 XML 可能有 200–500 个节点。其中大量是纯布局容器(FrameLayout、LinearLayout)、不可见的占位元素、装饰性分割线——这些对 LLM 完全无用,但会消耗 Token 并稀释注意力。
过滤规则很直觉:
- 不可见的节点 → 丢弃
- 纯布局容器(无
text、无content-desc、不可交互)→ 丢弃 - 有文字、有描述、或可交互 → 保留
过滤后,上面那棵树从十几个节点精简到六七个有意义的控件。
4.2.3、第三步:序列化为 LLM 可读的文本
这是三个工具的核心技术差异之一——过滤后的结构化数据,用什么格式喂给 LLM。
DroidBot-GPT 用编号列表——最简洁:
|
|
LLM 回复 [1] type "运动鞋" → 系统通过编号找到对应控件的 bounds 坐标 → 调用 adb shell input tap 执行点击或输入。简洁、Token 消耗低、回复格式可控,但丢失了控件的层级和空间关系。
GPTDroid 用自然语言描述——走得更远:
|
|
这个转换靠模板填充 + 启发式规则实现:根据 class 选描述模板(EditText → “输入框”,Button → “按钮”),再结合 bounds 坐标推断空间位置(“顶部"“下方"“底部”)。LLM 更容易理解控件之间的语义关系(“搜索框旁边有搜索按钮”),但 Token 消耗更高,且自然语言的模糊性增加了回复解析难度。
DroidAgent 用结构化 JSON——取中间方案:
|
|
结构清晰,LLM 回复也容易解析成程序指令。
三种策略的取舍本质上是 Token 效率 vs. 语义丰富度 的权衡:
| 序列化策略 | Token 效率 | 语义丰富度 | 回复可解析性 | 采用者 |
|---|---|---|---|---|
| 编号列表 | 高 | 低 | 高 | DroidBot-GPT |
| 自然语言 | 低 | 高 | 低 | GPTDroid |
| 结构化 JSON | 中 | 中 | 高 | DroidAgent |
4.2.4、这条管线暴露的一个关键限制
为什么要把这个翻译过程单独拿出来说?因为它直接暴露了一个容易被忽视的前提:LLM 的理解质量,上限卡在 GUI 树中语义信息的完整度。
如果一个 ImageButton 没有设置 content-desc,LLM 看到的就是"一个图片按钮(无描述)"——它无法判断这是"搜索"“返回"还是"分享”。如果一个自定义控件没有暴露标准的 Accessibility 属性,它在 GUI 树中可能是一个空节点,甚至完全不存在。
这意味着两件事:
-
开发者做的无障碍适配越完整,LLM 测试效果越好。 一份本来是给视障用户准备的
content-desc,现在成了 LLM 理解页面的关键信号。好的 Accessibility 标注同时服务了两个"不看屏幕的用户”——屏幕阅读器和 LLM。 -
WebView 和 Flutter 等自渲染框架在这里有天然劣势。 Flutter 的 Skia 引擎完全绕过了原生视图树,UIAutomator dump 出来的 XML 里可能只有一个巨大的
SurfaceView节点——所有按钮、文字、输入框的信息全部丢失。这也是第五节"挑战四"要展开讨论的问题。
4.3、GPTDroid:把 GUI 测试变成一场对话
2024 年发表于 ICSE(软件工程最顶级学术会议)的 GPTDroid[1] 把这个想法做到了极致。
4.3.1、核心思路
GPTDroid 提出了一个极富洞察力的类比:GUI 测试就像一场对话。 App 展示当前页面(“我长这样”),LLM 回复应该做的操作(“那我点这个按钮”)。
每一轮交互是这样的:
|
|
这里的关键创新:LLM 接收的是自然语言描述,而不是 XML 格式的控件树。这意味着 LLM 是在"阅读"页面而非"解析"页面——就像一个人类测试员看到屏幕后做出的判断。
4.3.2、功能感知的记忆提示(Memory Prompting)
如果只是简单地让 LLM “看一页、做一步”,LLM 很快就会原地打转——因为它不记得之前做过什么。这就像一个失忆的测试员,每次打开 App 都从头开始。
GPTDroid 的核心技术创新是引入了功能感知的记忆机制。每次请求 LLM 时,Prompt 中包含三层信息:
第一层:当前页面信息
|
|
第二层:已探索的功能列表(避免重复)
|
|
第三层:未探索的功能提示(引导深入)
|
|
这相当于给 LLM 一个"测试备忘录”,引导它去探索未覆盖的功能分支,而不是在已经测过的功能上反复打转。
4.3.3、实验数据
GPTDroid 在 86 个 Google Play 热门 App 上的实验结果:
| 对比方法 | Activity 覆盖率差距 | GPTDroid 独有发现的 Crash Bug |
|---|---|---|
| vs. Monkey | GPTDroid 高 32% | —— |
| vs. Stoat | GPTDroid 高 19% | —— |
| vs. Humanoid | GPTDroid 高 13% | —— |
| 总计独有 Bug | —— | 31 个(部分已被开发者确认修复) |
这个数据值得细说:
覆盖率提升 32% 意味着 GPTDroid 能走到 Monkey 到不了的深层页面。差距主要来自两个方面:(1)GPTDroid 能生成有意义的文本输入,打开了那"30% 的隐藏功能分支";(2)记忆机制引导 LLM 持续探索新区域,而非在已知区域打转。
31 个独有 Crash Bug 意味着这些 Bug 是所有传统工具(Monkey、Stoat、Humanoid、ComboDroid)都没发现的。这些 Bug 藏在需要特定输入或特定操作序列才能触发的深层路径中——正是 LLM 语义理解能力的"杀手级"应用场景。
4.4、DroidBot-GPT:最小可行产品的力量
几乎和 GPTDroid 同期出现的 DroidBot-GPT[2] 走了一条更"简约"的路线。它的架构一句话就能说清:
DroidBot(已有的 GUI 遍历工具)+ GPT API 调用 = DroidBot-GPT
就这么简单。复用 DroidBot 已有的 GUI 状态提取能力,把 GUI 树序列化成文本,问 GPT:“现在页面是这样的,下一步做什么?”
DroidBot-GPT 的价值不在于架构多精妙,而在于它验证了一个至关重要的假设:
通用的、未经微调的 LLM,仅凭自然语言提示,就能理解移动端 GUI 的语义并生成有效的测试操作。
在此之前,很多人怀疑这一点——LLM 是在互联网文本上训练的,它怎么可能理解 Android 的 XML 视图层级?DroidBot-GPT 用最低的实现成本证明了:能,而且效果不错。
这个"最小可行产品"降低了整个领域的准入门槛——它告诉所有人:你不需要训练专用模型、不需要构建复杂管线,只需要把 GUI 信息"翻译"成自然语言,就能利用 LLM 的推理能力做测试。
4.5、DroidAgent:从"被动回答"到"主动规划"
GPTDroid 和 DroidBot-GPT 的 LLM 都是被动的——系统给它看当前页面,它回答下一步做什么。这就像一个测试新手,领导指哪打哪,从不主动想"我接下来该测什么"。
DroidAgent[3] 把 LLM 升级为一个"高级测试工程师"——它会自主生成高层次的测试目标。
4.5.1、两层决策架构
DroidAgent 的核心是一个两层决策系统:
|
|
战略层负责"想"——决定测什么。它基于 App 的功能描述(从应用商店获取)和已探索的功能列表,推理"这个 App 还有哪些功能我没测过",然后生成新的测试意图。
战术层负责"做"——在具体页面上执行操作。它结合当前意图和页面信息,逐步推导操作序列。
4.5.2、为什么这很重要?
DroidAgent 代表了从"工具"到 “智能体(Agent)" 的跃迁:
| 维度 | GPTDroid(工具) | DroidAgent(智能体) |
|---|---|---|
| 目标来源 | 由记忆提示隐式引导 | LLM 自主生成 |
| 决策层次 | 单层(下一步做什么) | 双层(先想测什么,再想怎么做) |
| 测试的"目的性” | 中等 | 高 |
从 DroidAgent 开始,我们不再称这些系统为"测试工具",而是称为 “测试智能体(Testing Agent)"——它们有目标、有规划、有执行能力。
5、这三个工作的技术脉络
把三个开创性工作放在一起,可以清晰地看到技术演进的脉络:
| 维度 | DroidBot-GPT | GPTDroid | DroidAgent |
|---|---|---|---|
| 时间 | 2023.04 | 2023.10(ICSE 2024) | 2023.11 |
| 定位 | 概念验证 | 完整系统 | 进阶架构 |
| LLM 角色 | 逐步决策者 | 带记忆的决策者 | 自主规划者 |
| 页面理解 | GUI 树 → 文本 | GUI 树 + 自然语言 | GUI 树 + App 描述 |
| 记忆机制 | 无 | 功能感知记忆 | 意图级别记忆 |
| 核心贡献 | 验证可行性 | 覆盖率突破 | Agent 范式引入 |
它们共同完成了三个范式转换:
|
|
6、第一代 LLM 测试方法的四个挑战
尽管进展巨大,这一批方法仍面临四个共同挑战:
6.1、挑战一:每步都要调 LLM,太慢太贵
假设一次测试需要 300 步操作,每次 LLM 调用耗时 3 秒、费用 $0.01:
- 时间:300 × 3 = 15 分钟(Monkey 只需几秒完成 300 次操作)
- 费用:300 × $0.01 = $3/次
这个问题有解。GraphDroid 提出了一种"异步架构”,将 LLM 调用量降低了 90%——怎么做到的,值得单独展开。
6.2、挑战二:只在 Android 上验证,iOS / Flutter 怎么办?
这批方法都在 Android 上做实验。跨平台迁移能力没有被验证。
IntentDroid 给出了一个思路:把测试意图和平台执行解耦——一次提取意图,多端复用。
6.3、挑战三:Oracle 仍然只是 Crash 检测
“搜索结果对不对"“价格计算对不对”——它们判断不了。
已经有人在攻克这个问题了。VisionDroid 尝试通过"看截图"发现非崩溃功能 Bug,LogiDroid 则走了自动生成业务逻辑断言的路线——两个方向都值得深挖。
6.4、挑战四:依赖 GUI 树,WebView/Flutter 自渲染时信息残缺
多模态 LLM 提供了一条绕过路径:不解析 GUI 树,直接"看"屏幕截图来理解页面。
7、总结
本文回答了两个问题:
“传统测试为什么越做越痛?" → 因为机器不理解语义。控件 ID 会变、平台会不同、业务规则不在代码里——但语义是稳定的。
“LLM 如何破局?" → 三个工作渐进式地回答了这个问题:
- DroidBot-GPT 证明了 LLM 能做到
- GPTDroid 证明了 LLM 能做得好(覆盖率 +32%,发现 31 个独有 Bug)
- DroidAgent 证明了 LLM 不仅能执行,还能规划
但故事没有在这里停下。LLM 测试正在向两个更深的方向突破——跨平台迁移和视觉理解。如果 AI 不读 XML 控件树,而是直接"看"屏幕截图,它能发现什么样的 Bug?如果一套测试意图能在 Android、iOS、Flutter 上分别运行,跨端测试维护的噩梦是否就此终结?这两个问题的答案,比想象中更有趣。
参考文献
[1] Zhe Liu, Chunyang Chen, Junjie Wang, et al. “Make LLM a Testing Expert: Bringing Human-like Interaction to Mobile GUI Testing via Functionality-aware Decisions” (GPTDroid). ICSE 2024. arXiv:2310.15780.
[2] Hao Wen, Hongming Wang, Jiaxuan Liu, Yuanchun Li. “DroidBot-GPT: GPT-powered UI Automation for Android”. arXiv preprint, 2023. arXiv:2304.07061.
[3] Juyeon Yoon, Robert Feldt, Shin Yoo. “Intent-Driven Mobile GUI Testing with Autonomous Large Language Model Agents” (DroidAgent). arXiv preprint, 2024. arXiv:2311.08649.
[4] Ting Su, Guozhu Meng, Yuting Chen, et al. “Guided, Stochastic Model-Based GUI Testing of Android Apps” (Stoat). ESEC/FSE 2017.
[5] Ke Mao, Mark Harman, Yue Jia. “Sapienz: Multi-objective Automated Testing for Android Applications”. ISSTA 2016.
文章作者 calssion
上次更新 2026-10-11