工具与库 · 2026 年 11 月 11 日

Playwright:比 Selenium 更稳的浏览器自动化

端到端测试最贵的不是写用例,是排查那些「重跑就过」的失败。Playwright 把等待、隔离和现场留存做进了机制里,这篇文章讲清它为什么稳,以及哪些场景它并不合适。

一条 CI 流水线,端到端测试二十个用例,每次总有两三个失败。点开日志,报错是「元素不可点击」,重跑一遍又全绿。改了两周,把 sleep 从 1 秒加到 3 秒,失败率降下来了,整条流水线也从 8 分钟变成 15 分钟。

这类问题的根源不是产品有 bug,而是测试脚本在猜「页面好了没有」。Playwright 的出发点就是把这件事从「猜」变成「等一个明确条件」。

稳的原因:等待被写进了动作里#

Selenium 里点一个按钮,脚本执行完 click() 就继续往下走,页面没准备好是脚本的事。Playwright 的每个动作之前会做一组可操作性检查:元素存在、可见、尺寸稳定、没有被禁用、没有被别的元素遮挡。任意一项不满足就重试,直到超时。

断言也一样。expect(locator).to_be_visible() 不是查一次就返回,而是在超时时间内反复求值,条件成立立刻通过。这叫 web-first assertion,它替代了绝大多数手写的显式等待。

另外两个设计也直接减少 flaky:

  • 每个用例一个浏览器上下文。cookie、localStorage、缓存天然隔离,并行跑不会互相污染,比开一堆浏览器进程便宜得多。
  • 失败现场自动留存。trace 里记录了 DOM 快照、网络请求、console 输出和每一步的截图,事后能像回放一样看。

这三件事合起来的效果是可以量的:固定 sleep 的耗时随用例数线性增长,条件等待只在真正慢的那几步上花钱。验证方法很简单,同一条用例跑十次,看耗时分布而不是平均值——平均值会被最慢的一次拉偏,而 flaky 恰恰藏在尾部。

一个能跑的最小例子#

安装方式(pip、npm、包管理器)以官方文档为准,装完还要下载浏览器二进制,这一步别漏。

python
# pip install pytest-playwright && playwright install chromium
from playwright.sync_api import Page, expect

def test_login(page: Page):
    page.goto("https://example.com/login")
    page.get_by_label("邮箱").fill("user@example.com")
    page.get_by_label("密码").fill("not-a-real-password")
    page.get_by_role("button", name="登录").click()
    expect(page.get_by_role("heading", name="控制台")).to_be_visible()

注意这里没有任何 sleep,也没有 find_element 加 WebDriverWait 的组合。定位用的是角色和标签文本,不是 CSS 路径。

失败时抓 trace:

bash
pytest --tracing=retain-on-failure
playwright show-trace test-results/*/trace.zip

retain-on-failure 只在失败时保留,不会把每次运行的产物都堆在磁盘上。

定位器怎么选,决定脚本能活多久#

优先级从高到低,理由都是「抗重构」:

优先级 定位方式 适用
1 get_by_role 按钮、链接、标题等语义元素,最稳
2 get_by_label 表单字段,跟着 label 走
3 get_by_placeholder 无 label 的输入框
4 get_by_text 静态文案,注意文案会改
5 get_by_test_id 前四者都不合适时,让前端加 data-testid
6 CSS / XPath 最后选择,耦合 DOM 结构

用 get_by_role("button", name="登录") 的脚本,前端把 div 换成 button、把类名全改掉,测试都不用动。用 div:nth-child(3) > span 的脚本,一次样式重构就全红。

稳定性与迁移#

Playwright 给了机制,但下面几件事仍然要靠人:

  • 别用 wait_for_timeout。它是 sleep 换了名字,一旦出现就说明有个条件没找对。要等 URL 变化用 wait_for_url,要等接口用 wait_for_response,要等界面用 expect。
  • 登录态复用要管过期。storage_state 存下来能省掉每个用例重新登录,但 token 过期后会得到一堆莫名其妙的失败。CI 里每次运行重新生成,或者给失败用例加重试一次。
  • 测试数据自己造。依赖「数据库里恰好有一条 id=1 的记录」的用例,迟早会在并行执行时互相踩。
  • 第三方依赖要拦掉。统计脚本、字体、客服组件走 page.route 直接返回空响应,既快又不会因为外部服务抖动而红。
  • flaky 要当 bug 修。--repeat-each=10 跑十遍,把偶发失败稳定复现出来,再去看 trace,而不是直接加 retries。重试是止血,不是治疗。

CI 里通常用 pytest -n auto 按 worker 并行,一个 worker 一个浏览器。两个细节容易翻车:一是 trace 的产物路径要能区分 worker,否则互相覆盖;二是容器里缺系统库,浏览器起不来,官方提供的 playwright install --with-deps 在国内网络下经常卡住,提前烘进镜像更省事。

从 Selenium 迁过来,心智要换#

不是把 find_element 换成 locator 就完事,几个对应关系先理清:

  • 没有 WebDriverWait 和 driver 生命周期,fixture 直接给你 page;
  • 断言用 expect(locator),不要自己取 text_content() 再 assert,后者不会重试;
  • 多标签页用 context.expect_page() 拿新页面,不再靠切 window handle;
  • iframe 用 frame_locator(),链式写法不用先 switch_to;
  • 文件下载用 expect_download(),上传用 set_input_files()。

迁移顺序建议从最痛的地方开始:先挑最 flaky 的那几条用例改写,用「失败率下降」说服团队,比一次性重写全部脚本靠谱。

截图对比也能做(to_have_screenshot),但坑在字体渲染:本地和 Linux CI 出的图经常不一致。要么统一在容器里生成基线,要么只截关键区域。组件测试模式仍标注为实验性,落地前以官方文档为准。

什么时候别用 Playwright#

需求 建议
只测 HTTP 接口 用 httpx / requests,快一个量级
需要 IE 或很老的浏览器 不支持,换别的方案
移动原生 App 用 Appium 这类工具
大规模并发抓取公开网页 资源成本高,且要先看目标站点的条款
需要用户已登录的真实浏览器配置 能注入,但维护成本高,先想清楚是否必要

还有一条常被忽略:Playwright 跑得快,但每个浏览器上下文仍要占内存。CI 里 worker 数量要按机器内存调,开太多会 OOM,表现是随机超时,看起来像 flaky,其实是资源不够。

结论#

  • 把「等待」交给 expect 和自动等待,脚本里不该出现固定 sleep。
  • 定位器优先用 role / label,其次 test id,最后才用 CSS。
  • 每个用例独立上下文,数据自己造,第三方请求拦截掉。
  • 失败先看 trace,再决定重试;--repeat-each 是复现 flaky 的好工具。
  • 只测接口或要跑老浏览器时,别硬套这套工具。

完

#Playwright#自动化测试#浏览器#E2E
回到顶部 ↑