Playwright 自愈测试零成本:五个版本的探索之路

原文:https://dev.to/flashpeter7/self-healing-playwright-tests-for-0-five-versions-of-getting-there-93p(作者 @flashpeter7)

AI 测试工具五个版本:我试了什么、花了多少钱、哪里崩了

我想为电商店铺做自动化健康检查。标准需求:搜索能用、商品页正常渲染、购物车能加商品、结算流程别炸。

但有一个非标准需求:测试要用纯自然语言编写。不是代码,不是 DSL,也不是录制回放。一个文本文件写着「搜索电钻,打开第一条结果,加入购物车,确认购物车显示 1 件商品」——这就是整个测试。

为什么坚持这样?两个原因。写场景的人不应该需要维护 Playwright 选择器。选择器正是 E2E 测试的葬身之地——每次 UI 调整都会弄坏它们,维护成本悄然超过测试能捕获的 bug。

这个项目结束前,我经历了五个版本。路径并非直线——有一段我甚至故意往回走。以下是全部经过,死胡同一个不落。

v0:LLM 驱动浏览器

当今环境下的直观答案:把 LLM 放进执行循环。browser-use 之类的库封装了 Playwright,让模型直接根据自然语言指令驱动浏览器。

我在开发服务器上用 Docker 搭好了它。它确实能用——真的。模型读取指令、查看页面、点击正确的元素。

然后我看到了账单。跑几次就花掉真金白银,API 速率限制几乎立刻触发。而我本来打算把这些测试挂在 cron 上,每天跑很多次,永远跑下去。除了成本之外:每步延迟数秒,而且同一个测试可能通过也可能失败,取决于模型当天怎么解读页面。

杀死这个方案的是那个顿悟:每次执行都让 LLM 解读你的测试,等于为一件只需要动用一次智能的任务反复租借智能。 场景在多次运行之间不会变,页面(通常)也不会变。为什么要每小时付钱让模型重新摸索同样的点击?

结论:LLM 应该待在生成阶段。一次生成普通的 Playwright 测试,然后永远免费运行。此后每个版本,都是对同一个问题的不同回答:模型如何知道页面上有什么?

v1:书签小工具——人类来指点元素

第一个答案:让人来告诉它。目标页面上的一个书签小工具让我用鼠标点击元素;这些引用连同我的纯文本步骤一起被收集起来,发送给一个小型 Flask 后端,由模型组装成 Playwright 测试。外围配套:一个 /run 接口、pytest、cron 调度、截图报告,还有一个带代码编辑器的小型 Web UI 用来做修正。

它能用,生成也便宜(约 600 token——找元素的活人类已经干完了)。但它有一个结构性缺陷:页面加载时 JavaScript 没有被正确执行,所以客户端渲染出来的内容完全不可见。而且手动点击每个场景中的每个元素,恰恰是我想要消灭的那种手工劳动。

v2:无障碍树——让浏览器来指点元素

然后我发现,真正的答案其实一直藏在浏览器里:无障碍树(accessibility tree)。它就是辅助技术眼中的页面——角色、名称、状态、结构——所有 div 泥潭都不见了。Playwright 在 JS 完全渲染后把它导出为快照。

数字替我做了决定:一个真实的商品页约有 2MB HTML——几万 token 的噪声。同一个页面的无障碍快照只有约 2KB JSON、500–1000 token,而且恰好包含测试要交互的全部东西:按钮、链接、输入框及其标签。

于是 v2 诞生:你提供一个 URL 和一个纯文本场景,服务器无头加载页面、快照无障碍树,然后一个小巧廉价的模型(Haiku)根据场景 + 树写出测试。无需点击,无需书签小工具,JS 渲染问题解决。每个生成测试的成本:不到一分钱。

v2.5:在真实浏览器中逐步生成

从一份快照生成完整的多步骤测试,有一个明显的漏洞:页面会随着测试推进而变化。搜索页的树说明不了购物车页的任何情况。

于是生成循环变成了交互式的。通过 CDP 建立持久浏览器会话,一个智能体(用 Claude Code CLI 作为编排器)逐步工作:快照当前页面 → 为一个步骤生成代码 → 在真实浏览器中执行 → 快照新状态 → 生成下一步。就像人类在编辑器旁边开着浏览器写测试——区别在于每一步在写下的瞬间就对照现实验证过了。最后所有步骤被拼接成一个测试文件,pytest 确认通过,然后保存。

自愈(self-healing)能力也几乎就这样白捡到了。每个测试是一个包含三个文件的文件夹:

tests/add-to-cart/
├── scenario.txt   # 第 1 行是 URL,后面是自然语言步骤
├── tree.json      # 生成时的无障碍树快照
└── test.py        # 生成的 Playwright 代码


这份 tree.json 快照恰恰就是变更探测器。运行前抓取当前树,和保存的那份做 diff。没有差异 → 页面没变 → 原样运行现有测试,成本 $0。有差异 → UI 变动了 → 基于新树重新生成,保存新快照。模型只在真正需要它出力的时刻才付费:首次生成和自愈。执行则常驻在 GitHub Actions 的定时任务里;另一个容器化的报告服务负责收集截图和 pytest 输出。

v4:有意后退

这里就是来回反复的原因。无障碍树有一个弱点:有时你需要的元素根本不在里面——自定义控件、canvas、糟糕的标记结构。v2 的兜底方案是“手动把选择器粘贴进测试”,这等于让旧的维护问题又从窗户溜了回来。

所以 v4 回到了由人直接指向元素——但这次做对了:一个 Chrome 扩展。你在一个文本域里写场景,在真实页面上点击元素,扩展会把每个元素的完整 XPath 以内联形式插入到你的文本中:Click the login button [/html/body/div[2]/form/button]。状态通过 chrome.storage 在页面刷新后保留下来;提交时把文本 + XPath 发给服务器,由模型写出测试。

人的意图用自然语言表达,元素的精确身份由机械方式捕获,LLM 充当两者之间的翻译器。在 v2 模糊的地方它精确——在 v2 自动的地方它手动。两个版本谁也没压倒谁;它们只是把同一个问题来回抛给对方。

经济考量(我为什么一直做下去)

让我坚持迭代下去的是市场空白。AI 测试 SaaS——Momentic、Mabl、testRigor 之流——为同样的承诺收取每月大约 300–2000+ 美元:用自然语言写测试,UI 变化时自愈。企业级产品更是每年动辄数万美元,而你的测试被锁进它们的专有格式。

我的版本:每次执行 0 美元,每次 UI 变更只要几分钱,输出是归我自己所有的标准 Playwright 代码,跑在免费 CI 层上。这差距不是他们那边有什么魔法——而是架构。他们把模型(或平台)留在回路中;我则在每个测试的生命周期里只为智能付费两次。

最终去向

在犹豫要不要继续深入开发时,我发现 Playwright 现在已经自带内置测试代理(planner / generator / healer,自 v1.56 起),原生覆盖了同样的“生成一次、变更时修复”循环——自然是以无障碍树为驱动。同一个想法,由厂商维护的实现,在每个关键维度上都胜过个人框架:文档、生态,以及有人替你去修 bug。

于是我停了。只保留了针对自己环境的那部分胶水代码——调度、报告、截图。场景文件留了下来;框架没有。

对这段弯路,我不后悔。五个版本让我获得了阅读无法给予的东西:我知道了 为什么 无障碍树是正确输入,为什么 LLM 应该放在生成阶段,为什么 修复必须由 diff 触发——因为每一个替代方案都让我付出了金钱、时间,或者两者兼有的代价。这种理解是可以迁移的。代码则没有这个必要。

原文:https://dev.to/flashpeter7/self-healing-playwright-tests-for-0-five-versions-of-getting-there-93p(作者 @flashpeter7)

发布评论
全部评论(0)