← 返回文章选择

AI 写 Playwright 测试:把操作录制变成行为验证

录制只记下点了哪里;断言、语义定位、数据前提和可信的 CI 才让它变成测试。

录制一次操作,得到的是浏览器刚才走过的路径。要让这段记录成为长期测试,还得说清每一步为什么做、做完应该看到什么。role、label、testid、trace 和 Gherkin 这些关键词,都可以放回这个问题里理解。

生成代码之后,先查断言

初稿可以有几种来源:用 codegen 录制操作,让浏览器 Agent 探索动态界面,再由语言模型整理成测试。它们能帮着出初稿,初稿仍要审查。单纯重放点击,可能只证明了按钮能点、页面能切换。

比如表单提交后,测试要检查用户看得见的成功状态;任务要求持久保存的话,还要在合适的位置验证保存结果。导航正常,不能单独证明功能正常,要按任务要求看这两项检查做没做。

严格说,录制器也能录断言。codegen 文档里,工具栏上有三个断言按钮,分别对应“元素可见”“包含文本”“具有某个值”,点一下再点页面元素就行。所以录出来没有断言,多半是录的人没点,而不是工具做不到。生成器只负责把操作写下来,成功标准得人来定,文档自己也说生成完要手动再改。

浏览器 Agent 这条路,官方也做了自己的版本。Playwright 1.56 起内置了三个 test agents:planner 探索应用、写 Markdown 测试计划,generator 把计划变成测试文件,healer 跑套件、自动修失败的测试。配套的 Playwright MCP 读的是无障碍树,不看像素,所以它生成的定位器天然偏向 role 和文本,和下一节说的语义定位是同一套东西。

初稿跑起来后,失败信息会指向定位、数据和断言,这几项都要回去改。改完再跑,直到行为和预期一致。

测试初稿经人工审查和执行,失败后检查证据并返回修正

查看 Mermaid 源码
flowchart TD
scenario["路由、操作与预期行为"] --> record["录制或探索界面"]
record --> draft["生成测试初稿"]
draft --> review["人工检查定位、数据和断言"]
review --> run["执行测试"]
run --> result{"行为与预期一致?"}
result -->|否| evidence["检查 Trace、日志和实际页面"]
evidence --> review
result -->|是| ci["整理稳定用例并接入 CI"]

界面变了,别先改掉成功标准

如果只是布局变了、用户行为没变,就保留行为断言,调整定位器。这要求测试尽量从用户认得出的对象出发。Playwright 的最佳实践推荐测试用户可见的行为,优先采用面向用户的定位方式,少依赖 CSS 结构这类实现细节。

这条原则现在有了现成的反例。healer 的官方定义里,修复动作包括改定位器、调等待、修数据,目标是把测试重新跑绿。Michał Ślęzak 在 Sii 的实测文章里写到,最坏的情况是 healer 直接删掉一条断言,或者把场景大幅简化:

The test went green, the metric looked great, and the value of that test dropped to roughly zero.

他的结论是 agent 适合加速第一遍、收集证据,业务关键路径仍要人审。我的看法一样:自动修复的 diff 里只要动了断言,就该当成改需求来审,而不是当成修 bug。

定位器:语义、约定和唯一

role、label 和显式约定的 test id 都能表达定位意图,区别在于靠什么:role 和 label 靠页面自带的语义和标签,test id 靠事先约定。页面本来就缺语义和标签,单靠换一个定位器补不出来。

定位器文档把取舍写得很直白:role 最接近用户和辅助技术看页面的方式,所以优先;test id 最抗改,但它对用户不可见;CSS 和 XPath 绑死 DOM 结构,不推荐。

不管换哪种,都要避开布局一变就失效的写法。生成的选择器要在对应范围内唯一,不能因为“这次刚好点中了”就留下。Playwright 在这点上比我还硬:定位器是严格的,一个要求单一目标的操作匹配到多个元素就直接抛异常。用 first()、nth() 绕过去也被文档点名不推荐,页面一改就可能点到别的元素。

等待:等状态,不等时间

随手插 waitForTimeout 也会带来维护问题:环境稍慢就失败,稍快又白白等待。等待应该对应一个可观察的条件。API 文档直接把它标成 discouraged:

Never wait for timeout in production. Tests that wait for time are inherently flaky.

替代品是自带等待的两样东西。一是动作前的可操作性检查:可见、稳定、能接收事件、启用等,其中“稳定”指元素连续两帧包围盒不变。二是 web-first 断言,比如 await expect(locator).toBeVisible(),它会重试到条件满足,默认超时 5 秒;手写的 expect(await locator.isVisible()).toBe(true) 则一秒都不等,异步渲染的页面就会被判成偶发失败。

有个团队的六个月复盘给了一个数字:他们加了一条 ESLint 规则禁用 page.waitForTimeout,下个月 flaky 降了四成。这是单个团队的观测,不是对照实验,但方向和官方说法一致,而且这条规则成本几乎为零。

失败时再结合 trace、截图和日志看过程,别只凭最后一张 DOM 快照猜原因。trace 给每个动作存动作前、动作瞬间、动作后三份完整 DOM 快照,外加网络请求、两侧 console 和对应源码行,信息量比最后一张截图多得多。

复杂流程要把数据前提写出来

动态数据、条件分支、OAuth 重定向和过长流程,都会增加生成难度,因为它们要求测试写明前置状态。比如同一个按钮在不同身份下结果不同,题面就要说清用哪个身份、哪种数据,预期落在哪条分支。

前提写清之后,落到代码里有现成的位置。登录这类状态,官方的认证文档建议在 setup project 里登一次、把状态存下来给所有测试复用,并行时每个 worker 用自己的账号;这样 OAuth 重定向只走一遍,而不是每个用例重走。注意存下来的状态文件里可能有敏感 cookie,别提交进仓库。其余数据前提放进 fixture,它的定位就是“给测试它需要的一切,别的不给”,并且每个测试之间隔离。

十几步混成一段话,生成者很容易漏掉中间状态。拆分时围绕独立的用户行为建用例,同时保留确实需要覆盖的跨步骤流程。照片里出现的“十步以内”“五十个测试以上”这些数字没有测试条件,不能当成 AI 能力的固定阈值。

带条件的数字是什么样,可以看 Slack 工程团队的 Sergii Gorbachov 写的一份实测:

条件 结果
规模 200 多次 agentic E2E 运行,每种配置每条流程 20 次
流程长度 简单约 15–20 步,中等约 25–30 步
生成 Playwright 测试 最快(约 3 分钟);失败率简单流程约 8%,复杂流程约 48%
MCP 方式 失败率 0–12%
动作序列一致性 只有约 20% 的运行走了完全相同的动作

失败原因他归到 UI 状态多变和抽象错配上,结论是 agent 驱动的 E2E 不能替代确定性测试。数字要连同步数、次数和方式一起引用,单拿一个 48% 出来就又成了没有条件的阈值。

CI 的结果要让工程师愿意相信

第二批笔记转向 Docker、worker、分片和资源成本。这里缺机器配置和测量记录,所以不保留具体的内存数字。值得留下的是三个问题:

  • 一次运行启动多少 worker?
  • 重试消耗了多少时间?
  • 能不能从报告里找到失败原因?

这三个问题,官方文档都给了默认答案:

问题 官方口径
worker 数 CI 文档建议 CI 上设为 1,优先稳定和可复现;要更快用分片拆到多个 job,并且一定要设 globalTimeout
分片的代价 --shard=x/y 能并行缩短总时长,但要先出 blob report 再 merge-reports,想负载均衡还要开 fullyParallel: true
重试 默认不重试;首跑失败、重试通过的测试被标为 flaky
失败证据 CI 上推荐 trace: 'on-first-retry',不开重试就用 retain-on-failure

worker 是独立的操作系统进程,各自起浏览器,彼此共享不了任何状态;测试一失败,这个 worker 就被关掉重开。所以靠全局变量串起来的测试,在并行下会表现成偶发失败,每次重试还要付一次冷启动。

噪声多的测试会吃掉排查时间。针对同一个失败反复重试,只能说明它有时能通过,Playwright 管这叫 flaky;还要继续定位是产品缺陷、数据依赖还是环境问题。官方把测试隔离写成硬要求,理由正是隔离能防止级联失败、让调试变简单,三种原因也才分得开。对有价值的流程,失败信息要具体到能支撑下一次修正。

如果现在要接 AI 生成的测试,我会先做三件事:lint 禁掉 waitForTimeout;healer 改动断言一律人审;CI 开 on-first-retry 的 trace。前一份复盘里,降幅最大的一步是对超过三个用户动作的测试强制人工审查,这和第一节“初稿仍要审查”是一回事。

留下几句话 读者来信

读到这里,有什么想法?欢迎留下你的看法。