游乐游手机版
首页/AI教程/文章详情

AI原生工程Agent自动化测试实践

时间:2026-06-04 17:04
单元测试能够验证一个函数的计算结果是否正确,却无法告诉你 splash 页面究竟有没有成功跳转到主页、消息发出后回执是否正常返回、某个按钮点击后界面有没有如预期响应。这类问题只有把 app 真正运行在真机上、亲手点一下才能确认。过去,这一环节完全依赖人工:连接真机、复现操作路径、截图、翻阅日志,跑完

单元测试能够验证一个函数的计算结果是否正确,却无法告诉你 splash 页面究竟有没有成功跳转到主页、消息发出后回执是否正常返回、某个按钮点击后界面有没有如预期响应。这类问题只有把 app 真正运行在真机上、亲手点一下才能确认。过去,这一环节完全依赖人工:连接真机、复现操作路径、截图、翻阅日志,跑完一轮像样的回归测试往往要耗费一个人小半天的时间。

工程团队搭建了一条自动化链路,把这件事交给 AI agent:给它一份编写好的测试剧本,它自己连上正在运行的 app,按照步骤点击、输入、滑动,执行完毕后生成一份完整报告。这条链路被称为 app-operator。它底层依赖一个名为 Marionette 的插件,我们先从这个插件说起。

一、Marionette:让 AI 既能“看见”也能“操作”一个 app

Marionette 的功能可以一句话概括:将“操控一个正在运行的 Flutter app”变成一组 AI 可以直接调用的工具。仓库地址

它分为两部分:

一部分嵌入在 app 内部,即 marionette_flutter 这个包。它提供了一个 MarionetteBinding,在 app 启动时替换默认的 WidgetsFlutterBinding,挂载之后,app 的 widget 树、日志、渲染画面就能通过 Dart 的 VM Service 调试通道被外部读取和操控。换句话说,它在 app 身上开启了一个“可以被远程遥控”的接口。

另一部分运行在 app 外部,即 marionette_mcp 这个 server。它将上述接口翻译成 MCP 协议的工具,连接到 Claude、Codex 等 agent 上。工程根目录下的 .mcp.json 中已经配置完毕,使用工程内的 fvm dart 来启动:

"marionette": {"command": "zk_app/.fvm/flutter_sdk/bin/dart","args": ["pub", "global", "run", "marionette_mcp"]}

连接成功后,agent 获得的能力分为两类。一类是“看”:抓取 app 从启动到当前的全部日志、截取当前屏幕、读取当前屏幕上所有可交互 widget 的清单(每个元素的 key、文本、类型、坐标)。另一类是“操作”:按文本点击、按 key 点击、按坐标点击、长按、双击、向输入框输入文字、滑动、返回。

到了这一步,事情的性质发生了变化:agent 不再仅仅是“编写测试代码”,而是能够像人一样,看着屏幕、按照步骤,把 app 操作一遍。

二、工程里的 app-operator 链路

插件提供了基础能力,但真正让它在团队中可复用、可留痕的,是围绕它搭建的这条完整链路。下面按照“一次回归测试是如何执行起来的”顺序来讲解。

2.1 它负责什么,不负责什么

app-operator 是工程中的一个 agent(定义在 .claude/agents/app-operator.md,运行在 sonnet 上)。它的职责非常专一:给它一个 spec 剧本,它按剧本驱动 app、执行完毕、输出报告。它不负责构建、不负责安装包、不负责登录——这些前置环境由调用方负责,它只负责“按剧本操作”这一环节。

工程中还有一个容易与之混淆的角色,即 marionette-debug skill。两者都使用 Marionette,但分工正好相反:skill 用于临时诊断——“app 现在卡在哪里了,快速看一眼日志和截图就走”,尽量不改变 app 的当前状态;app-operator 用于回归测试,它会真正去点击、去断言、改变 app 状态,并且每次都生成一份报告。简单来说:想查看当前状态用 skill,想按剧本执行回归测试用 agent。这条边界看似琐碎,但至关重要——诊断的前提是“不动现状”,回归的前提是“按预期改变状态”,两个任务混在一个角色里迟早会出现问题。

2.2 让 agent 自己找到入口(这条链路最关键的一环)

这一段是整条链路能够实现“无人值守”的真正原因,但原理并不复杂——只是一个日志重定向。

背景是这样的:agent 要操控 app,必须先连接到 app 的 VM Service,而该地址形如 https://127.0.0.1:60789/6i_RqcARFZ0=/,端口和 token 每次执行 flutter run 都会变化。如果每次都需要人工把这串地址复制出来粘贴给 agent,那么这套方案就谈不上自动化——人仍然被卡在中间。

解决方案是给启动过程增加一层固定约定。工程中使用 make dev 来启动 app:

dev:@cd zk_app/apps/zhike && fvm exec flutter run --fla vor beta --dart-define=FLA VOR=beta $${DEVICE:+-d $$DEVICE} 2>&1 | tee /tmp/zhike-flutter-run.log

关键在于末尾的 tee /tmp/zhike-flutter-run.log:它将 flutter run 的输出原样复制一份到固定路径。VM Service 的地址会被 Flutter 打印在这份输出中,于是 agent 无需任何人协助,自己就能完成以下流程:

  1. 读取 /tmp/zhike-flutter-run.log
  2. 用正则 VM Service on .* is a vailable at: (https?://[^s]+) 提取地址,取最后一次匹配(app 可能重启过,最后一次才是当前有效的那个);
  3. https://...:60789/TOKEN=/ 转换成 WebSocket 地址 ws://...:60789/TOKEN=/ws(更换协议、末尾追加 ws);
  4. 连接进去。

就这么简单。没有服务发现、没有额外进程,一个 tee 加一段 grep,就把“人工粘贴地址”这个唯一的人工瓶颈消除了。这类不起眼的工程化细节,往往才是一条自动化链路能否真正跑起来的关键分水岭——能力是由插件提供的,但“让能力自动接上”需要自己缝合。

顺便提一下,这也是它的一个薄弱点:app 一旦 hot restart 或重启,地址就会改变,必须重新 grep 获取最新一次。因此 agent 每次连接前都会重新读取日志并取最后一条,而不是缓存旧的地址。

2.3 Spec:把“怎么测”写成可版本化的剧本

agent 不会自由发挥,它严格遵循 spec 执行。spec 就是用 Markdown 编写的测试剧本,存放在 docs/app-operator/specs/ 目录下,采用五段式结构:Setup(初始状态)、Steps(逐行操作)、Assert(最终验收)、Teardown(清理恢复到初始状态)、Notes(已知的不稳定点和假设)。

来看工程中一条真实的冒烟测试 spec send-text-message,它验证的是“在某个群组发送一条文本消息,发送成功、回执到位、输入框清空”:

## Setup- 当前在消息列表首页(底部 Tab "消息" 高亮),已登录- 列表里有 "测试礼物" 这一条## Steps1. `tap: "测试礼物"`2. `wait_for_text: "33 成员"`3. `tap_coordinates: [150, 704]` # 聚焦输入框(TextField 没 ValueKey,暂用坐标)4. `enter_text: "operator smoke test 一条消息"`5. `tap_coordinates: [324, 419]` # 发送按钮(无名 IconButton)6. `wait_for_key: receipt_status_sent`## Assert- `visible_text: "operator smoke test 一条消息"` # 气泡文本- `visible_key: receipt_status_sent` # 已送达图标## Teardown- `press_back`- `wait_for_text: "搜索全站动态、项目、圈子"` # 回到消息列表

每个动词对应一个 Marionette 工具:tap / tap_key / enter_text / scroll_to / wait_for_text 等等;断言方面有 visible_text / visible_key / absent_text / 按类型统计数量等几种谓词。编写 spec 有一个原则:能用文本或 key 定位的就不要使用坐标,坐标是最后的选择,如果使用必须在 Notes 中写明假设——上面那条 spec 之所以使用 [324, 419] 来点击发送按钮,是因为该按钮尚未添加 ValueKey,这一点在 Notes 中已经专门记录。

把测试写成 spec 而不是一段一次性的对话,好处在于它可以纳入 git 版本管理、可以 review、可以反复运行、可以不断演进。它是一份资产,而不是一次操作。

2.4 一次 run 的完整生命周期

agent 接收到 spec 后,会执行一条固定的流水线,每一步都配有明确的失败处理机制。

连接:按照 2.2 的方法获取 ws 地址,进行 connect。如果返回 "No isolate found with ext.flutter.marionette...",说明 app 没有挂载 binding(多半是因为使用 release/profile 模式运行),此时直接中止并生成报告,不进行无谓的尝试。

Setup 校验:spec 的 Setup 部分描述了“应该从哪个页面开始、能看到哪些元素”。agent 首先执行 get_interactive_elements 读取当前屏幕,与 Setup 中的关键词进行比对。如果对不上就中止,报告中写明“预期 X,实际 Y”——绝不在错误的初始状态下强行执行,那样只会产生一份无意义的失败报告。

执行 Steps:按照顺序执行,每一步记录动作、结果和耗时。这里有一条最重要的纪律——单步失败立即中止,不进行重试:如果某一步 Marionette 报错,或者 wait_for_* 在超时(默认 5 秒)内没有等到目标元素,agent 立即停止,截图、抓取最近的日志写入报告,然后执行收尾。不重试是刻意设计的:重试会把“偶发”和“真坏”混淆在一起,掩盖真正的问题。异步操作是常态(IM 消息、接口回包都不是同步的),因此剧本中通过 wait_for_* 轮询来等待,而不是使用 sleep 硬等一个拍脑袋的时长。

Assert:Steps 执行完毕后,逐条验证 Assert,每条都通过 get_interactive_elements 检查当前屏幕。

Teardown:无论前面的步骤是否成功,都必须执行,尽力将 app 清理回初始状态(通常是返回到消息列表),确保下一条用例从干净的状态开始。如果无法恢复,在报告中给出 warn 提示,但不会反复重试。

整个过程还遵循几条硬性规则:不修改 Flutter 代码、不执行 hot reload、不杀掉 app、不自己“探索”剧本之外的路径、遇到意外的弹窗就截图报告并中止,而不是擅自点击关闭。这些约束的共同目的是——让 agent 的行为可预测。一个会自由发挥的测试 agent,跑出的结果是无法让人信任的。

2.5 报告与可追溯

每次执行完毕后,agent 会在 docs/app-operator/runs/<时间戳>-.md 生成一份报告:包含 VM 地址、spec 路径、每一步的结果表、Assert 的逐条勾选、失败详情(附带日志片段)、Teardown 状态。随后,它还会将这次记录回写到 spec 文件末尾的 History 表中,最新记录排在最上面:

| 时间 | 结果 | 报告 ||------|------|------|| 2026-04-22 15:19 | ✅ PASS | `runs/2026-04-22-151930-send-text-message.md` || 2026-04-22 15:21 | ❌ FAIL(step 5 超时) | `runs/2026-04-22-152100-recall-last-message.md` |

这样一来,“这条用例最近有没有运行过、是否通过、报告在哪里”,站在 spec 这一端就能一目了然;反过来,从某份 run 报告也能查到它对应的是哪条 spec。spec 和 run 互相指向,形成一条可追溯的链条。

将其放回整体架构图中来看:逻辑层有单元测试和 Widget 测试,各自有专门的工具去覆盖;而“在真机上实际点一遍看是否正确”这一块,过去一直是空白,现在由 app-operator 填补了。一个 agent,一份剧本,一份可以追溯的报告——它替我们完成了那项最耗费人力、又最容易被省略的任务,并且把它变成了可以反复执行的事情。

来源:https://juejin.cn/post/7646820023196123145
上一篇Claude Code Harness 05 五层能力分工机制全面解析 下一篇Kimi-WebBridge让浏览器自动化变得更简单
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
手把手教你免费获取小米MiMo百万亿Token及Claude Code配置全流程
AI教程 · 2026-06-04

手把手教你免费获取小米MiMo百万亿Token及Claude Code配置全流程

前言:百万亿Token免费额度领取指南 近期,小米MiMo大模型推出了重磅福利——百万亿Token的免费额度,申请流程极为简便,额度也十分充足,并且支持直接接入Claude Code等主流工具。本文将完整演示从注册申请、获取API密钥,到最终在Claude Code中完成配置的全流程,跟着操作即可轻

Sentinel-3B OLCI L3全球降分辨率叶绿素数据2022.0版
AI教程 · 2026-06-04

Sentinel-3B OLCI L3全球降分辨率叶绿素数据2022.0版

Sentinel-3B OLCI Level-3 Global Mapped Earth-observation Reduced Resolution (ERR) Chlorophyll (CHL) Data, version 2022 0 叶绿素a浓度全球网格化数据集简介 叶绿素a浓度是衡量海洋浮

我每月省千元组建一支全天候云端AI团队
AI教程 · 2026-06-04

我每月省千元组建一支全天候云端AI团队

先说个有意思的现象。 前两天,我的视频生成团队“入职腾讯”了。在WorkBuddy专家团里,不少伙伴已经开始用这个工具做短视频。本来以为这事儿就这么定了,结果这两天,反而开始疯狂返工——我发现它只能生成文字驱动的视频,还不能像真正的视频团队那样,把配图的活儿也给干了。 于是,继续优化。 先给你看个好

如何编写合格的AI工作流指令:提升编辑技能
AI教程 · 2026-06-04

如何编写合格的AI工作流指令:提升编辑技能

如何编写一个合格的 Skill:AI 工作流核心指令集指南 在 AI 工作流的实际应用中,Skill(技能指令)常常被误解。许多人将其与普通提示词(Prompt)混淆,导致写出的指令过于宽泛或模糊,AI 难以精准执行。实际上,Skill 的本质是一套结构化的行为指令集,它引导 AI 助手在特定场景下

TRAE AI编程入门第三讲:Rules、Memory、MCP与Skills突破边界
AI教程 · 2026-06-04

TRAE AI编程入门第三讲:Rules、Memory、MCP与Skills突破边界

最近几天我会逐步公开自己策划的系统化 AI 编程入门课程大纲,欢迎各位提出宝贵建议。 这套课程暂定 4+1 节:4 节主课以 TRAE 为载体,带领大家零基础入门 AI 编程;外加 1 节扩展课,专门为非技术背景的学员补充软件工程基础知识。具体安排如下: 第一节:TRAE AI 编程入门——Vibe