在用 Midscene 做自然语言 UI 自动化之前,我大概有一年多的时间是跟 XPath 死磕过来的。当时接到的大部分测试任务其实没什么技术难度,无非是打开页面、点几个按钮、填几个表单、核对一下结果,可真正让人头大的反倒是“定位元素”这件事本身。页面改版、前端组件库升级、节点层级调整,任何一个细微变化都可能导致测试脚本当场废掉。后来我开始尝试用 Midscene 这种基于大语言模型的自然语言驱动方案,测试用例从“写选择器”变成了“描述意图”,开发效率高了不少,脚本的稳定性也提升了。这篇文章我想把完整的技术思路、实现步骤和一些踩坑经验整理出来,给同样在维护 UI 自动化测试的朋友做个参考。
1. 痛点拆解:XPath 为什么成了 UI 自动化的主要瓶颈
1.1 天天在踩的三类定位坑
早期写 UI 自动化,最常用的定位方式无非三种。第一类是绝对路径,比如/html/body/div[2]/div[3]/div[1]/button,看着就很脆弱,只要页面结构调整一层,整个路径就失效了。第二类是相对路径加属性筛选,比如//button[@class='submit-btn'],稍微好一点,但 class 经常拼接动态样式,属性值一变就找不到。第三类是依赖文本内容,例如//span[contains(text(),'确认')],这种在页面多语言切换或者文案调整的时候特别容易翻车。
这三种写法本质上都是把测试脚本和页面结构强耦合在一起。前端开发不可能保证 DOM 结构永远不变,组件化之后一个按钮的类名可能由三个模块拼接出来,再加上 A/B 实验、灰度开关、埋点属性,选择器写起来越来越费劲。我见过一些项目组为了稳定,专门给每个要自动化测试的元素加上>mkdir midscene-demo && cd midscene-demo npm init -y npm i @midscene/web playwright npx playwright install chromium
这里装了三个东西:Midscene 核心库、Playwright 浏览器控制库,以及 Chromium 内核。如果你已经有本地浏览器环境,也可以指定使用系统浏览器,不过为了跑测试的稳定性,我建议直接用 Playwright 自带的 Chromium,版本一致、行为可控。
安装完成之后,确认一下 Node.js 版本。Midscene 对异步 API 和部分新语法有版本要求,Node 18 及以上比较稳妥,太低会碰到一些兼容性问题。
3.2 模型选择与 API 配置
既然核心是自然语言理解,那一定需要接入大语言模型。Midscene 在模型接入上做得很灵活,既支持云端商用模型,也支持接入本地模型服务。云厂商的模型识别精度高,适合业务复杂、需要快速落地的场景;本地模型则胜在数据不出内网、调用成本低,但识别效果需要根据页面复杂度多做几轮尝试。
配置方式也比较直接,通过环境变量或者初始化参数指定模型类型和密钥即可。以 OpenAI 兼容接口为例,在项目根目录加一个.env文件:
OPENAI_API_KEY=sk-xxxx OPENAI_BASE_URL=https://api.openai.com/v1如果用的是国内服务商或者本地部署的工具,只需要把OPENAI_BASE_URL改成对应服务的地址。这种标准接口设计大大降低了切换成本。
3.3 最小可用示例:让 Agent 在浏览器里跑起来
配置好之后,写一段最简代码验证环境是否正常:
import { chromium } from 'playwright'; import { createAgent } from '@midscene/web'; const browser = await chromium.launch({ headless: false }); const page = await browser.newPage(); const agent = await createAgent({ page, model: 'gpt-4o', apiKey: process.env.OPENAI_API_KEY, }); await page.goto('https://example.com/login'); await agent.prompt('点击页面上的登录按钮'); await browser.close();如果环境正常,你会看到浏览器自动打开页面,稍微停顿后执行点击指令。停顿是因为模型推理需要时间,首次调用还会做模型预热,速度稍慢是正常的。这一步跑通了,说明基础链路已经打通,可以开始编写更复杂的测试流程。
4. 实战:用一段自然语言脚本完成电商购买流程
4.1 测试场景设计与前置准备
为了直观展示自然语言自动化的价值,我设计了一个模拟场景:打开一个电商 Demo 页面,搜索“无线耳机”,按价格区间筛选,进入商品详情页,加入购物车,最后确认购物车数量和金额。这个流程覆盖了输入、点击、筛选、跳转、数据校验几类典型操作,足够说明问题。
场景不用真实站点,我建议开发一套本地静态 Demo,或者用公司内部的模拟项目。重点在于验证流程,而不是测试外部网站的服务稳定性。另外,为了后续做断言,Demo 页面最好能展示明确的商品价格和购物车汇总数据。
4.2 用自然语言脚本描述业务步骤
下面这段代码完整实现了上面的业务场景:
import { chromium } from 'playwright'; import { createAgent } from '@midscene/web'; const browser = await chromium.launch({ headless: false }); const page = await browser.newPage(); const agent = await createAgent({ page, model: 'gpt-4o', apiKey: process.env.OPENAI_API_KEY, }); await agent.prompt('打开电商首页,地址是 http://localhost:8080'); await agent.prompt('在页面顶部的搜索框中输入“无线耳机”,然后点击搜索按钮'); await agent.prompt('在搜索结果的左侧筛选区中,点击价格区间“300-500”'); await agent.prompt('在结果列表里点击第一件商品进入详情页'); await agent.prompt('在详情页面点击“加入购物车”按钮'); const cartResult = await agent.prompt('打开购物车页面,告诉我商品名称和总金额'); console.log(cartResult.text); await browser.close();代码逻辑非常接近自然语言描述。写传统自动化的时候,每一步都要先去找元素、确认表达式、编写点击事件,然后调试半天;现在只需要描述意图,Midscene 自己完成元素识别和执行。
4.3 操作背后的关键细节
这段代码里有两个容易被忽略的细节。一个是agent.prompt返回的结果对象里带有text字段,它保存的是模型对页面信息的提取结果。比如我问“购物车里有什么”,返回的 text 会是一段总结文本。如果你的断言需要结构化数据,可以在提示词里要求“只返回 JSON 数组”,效果更好。
另一个细节是搜索框描述。我用了“页面顶部的搜索框中”这个位置限定描述,而不是只写“输入无线耳机”。这是因为页面上可能同时存在多个搜索框,比如页面顶部全局搜索和侧边栏分类搜索。加上位置限定可以减少模型误判。指令描述得越具体,识别准确率就越高。
4.4 断言与智能体返回结果校验
断言是测试的灵魂。Midscene 并不排斥传统断言,反而是建议把自然语言提取和代码断言结合起来。购物车场景中,我可以让 Midscene 把总金额返回出来,再由我们手动断言是否等于预期值:
const cartResult = await agent.prompt('读取购物车中所有商品的价格总和,只返回数字'); const actualTotal = Number(cartResult.text); if (actualTotal !== 429) { throw new Error(`购物车金额异常,期望 429,实际 ${actualTotal}`); } console.log(`断言通过,金额 ${actualTotal}`);这个组合的好处在于:元素定位和页面理解交给模型,数据校验和逻辑判断交给测试代码,两边各干各擅长的事。业务规则层面还是需要用代码来保证确定性,不能把“结果是否等于 429”这种判断也交给大模型,毕竟模型存在随机性,不能依赖它做严格断言。
注意:大模型的输出存在不稳定概率,涉及金额、数量这类精确值断言,一定要用代码判等,不能只依赖模型的总结。
5. 常见问题、排查技巧与稳定性优化
5.1 页面改版后测试失败,怎么定位问题
自然语言方案虽然耐造,但也不是完全免疫页面变更。遇到失败时,我的排查顺序是这样固定的。第一步抓取截图和页面快照,看看 Midscene 是在哪一步停下来的。第二步看最后一步的指令和前后页面的差异,比如页面是否弹了新框、按钮是否被遮住。第三步降低模型温度,把“思考”空间压缩,让指令执行更保守。
有时候页面元素明明在,但模型就是找不到,大概率是界面上的可点击区域被浮层遮挡了。这时候先滚动页面,或者先关闭弹窗提示,再执行目标动作。我把这条经验写在几乎所有项目里:复杂页面执行前,先处理弹层。
还有一个高频坑:中文页面和英文页面混合的场景。模型的识别依赖训练语料,遇到中英混杂的按钮文本容易犹豫。我的解决方案是在指令里给足上下文,比如“点击右上角蓝色的‘Join’按钮”,把颜色、位置、英文文本都带上,基本不会误判。
5.2 提示词表达的三条实操经验
自然语言自动化的效果很大程度上取决于怎么“说话”。我自己总结的三条经验,基本能覆盖大部分使用场景。
第一条,指令要有动词和目标。别只说“搜索框”,要说“在搜索框输入关键词后,点击搜索按钮”。模型不知道你想对搜索框做什么,必须描述完整动作。
第二条,能用位置限定就用位置限定。像“左侧菜单”“顶部导航”“结算区域”这些位置词,能显著缩小匹配范围。不是每个页面元素都有独特文本,位置是最强的约束条件之一。
第三条,复杂操作拆分成多个步骤。有些场景涉及多级筛选、多维配置,比如先选城市、再选配送方式、最后填优惠券。一次指令塞下所有动作,模型往往会漏步骤。分成多步执行,每一步都观察结果,稳定性会大幅提升。
5.3 性能、超时与稳定性实战数据
自然语言自动化的最大短板就是慢。一次动作执行要经过截图、传送、模型推理、执行、反馈,整个链路走完大概需要两到五秒。如果一个测试流程有三十个操作步骤,单条用例的执行时间就会达到两三分钟。这比传统 Click、Input 方式慢了十倍以上。
针对执行速度,我会做两件事。一是把高频回归场景中的稳定部分用传统方式固化,只把易变的业务步骤交给 Midscene 处理,形成混合模式。二是给模型响应设置合理超时,默认 60 秒比较合适,如果模型推理进程卡住,不用等半天再报错。
稳定性方面,一个典型问题是画面里有动画时,模型截图看到的可能是动画中间态。我的建议是在执行关键操作前加一个小等待,或者在指令里注明“等待页面动画结束后再操作”。实际跑下来,这种细节能把偶发失败率降低不少。
6. 客观边界:自然语言 UI 测试不是万能药
6.1 适合与不适合的场景清单
我经常跟团队里的小伙伴说,自然语言测试是一种很好的补充武器,但要清楚它的适用对象。用它处理业务流程复杂、UI 变化频繁、覆盖场景多端的用例非常合适,比如订单状态流转、权限配置验证、多语言站点逻辑测试。
但用它去做数据驱动的重复回归,效率反而低。设想一下要遍历一百个商品的价格和库存,传统代码循环加正则断言很快能跑完,用自然语言去解析每一个页面,成本和耗时都难以接受。
| 适合场景 | 不适合场景 |
|---|---|
| 业务流程长、状态分支多 | 单纯的数据遍历和表格校验 |
| UI 组件更新频繁、选择器不稳定 | 千篇一律的批量表单录入 |
| 功能验收、探索性测试 | 需要毫秒级执行速度的回归 |
| 动态元素、图表、富交互页面 | 依赖严格数据格式的接口回归 |
还有一种场景我会直接建议不用:对大量页面同时做快照式截图对比。Midscene 的优势是理解,不是批量对比,这类需求用像素级对比工具更合适。
6.2 成本与收益怎么算
成本主要在三块:模型 API 调用费、执行时间和调试成本。API 费用方面,一次动作大概需要几千个 token,按当前主流模型价格算,跑一条几十步的用例,模型成本大概几分钱到几毛钱。看起来不贵,但如果每天跑几百条用例,累计月成本就会比较可观。
执行时间和模型时钟延迟是隐藏成本。传统脚本三十步可能只要三十秒,Midscene 可能要两三分钟。这个差距在 CI 阶段尤其明显,直接拉长发布等待时间。所以我的建议是:在流水线里把自然语言用例的数量控制住,保证关键业务链路覆盖,等码刷新时再全量跑。
投资回报其实也清晰:维护成本下降是最直接的收益。传统选择器一改改半天的项目,换成自然语言后,大部分页面更新不影响测试脚本,这部分人工工时省下来,远超模型 API 的费用。
6.3 结合传统自动化工具形成混合方案的最优解
最实用的做法不是“全部替换”,而是“混合编排”。对于核心且稳定的页面结构,比如登录模块、通用导航,继续用 Playwright 自带的getByRole、getByTestId这类可靠定位方式,跑得快还稳定。对于业务逻辑复杂、页面结构易变的部分,比如购物结算、设置向导这种多状态流程,用 Midscene 的自然语言指令来驱动。
这样的混合方案兼顾了速度、稳定性和维护成本。我最常遇到的使用方式是:外层借用传统断言做数据判断,中间步骤使用自然语言做路由引导,最终回归时把高频稳定路径用原生选择器固化,低频复杂路径保留模型驱动。
用了一段时间以后,我个人最大的感受是:自然语言自动化的核心价值不是“省掉写代码”,而是让测试脚本的表达方式从面向实现细节变成了面向业务意图。这句话说出来容易,真正把它融入测试架构设计却需要一个过程。我更建议先从一条脆弱的痛点用例开始,把 Midscene 接到现有框架里跑通,再逐步扩大覆盖范围。这样既能验证实际效果,也能控制成本和风险,比一次性把整个测试体系推倒重来稳妥得多。