☰
Playwright替代Selenium:自动等待、浏览器上下文与网络拦截实践
2026/10/21 9:40:36 网站建设 项目流程

如果你还在用 Selenium 写 Web UI 自动化,一定体会过那种“等待谜题”:明明元素已经出现,一跑就报错;换一台机器,用例说挂就挂;想验证某个接口返回,还得抓包或者起服务。这些问题不是你的技术问题,而是框架设计的天花板。Playwright 这两年被越来越多测试开发团队拿来替代 Selenium,不是因为它“新”,而是因为它把 UI 自动化里最磨人的细节直接解决掉了。

这篇文章从一线测试开发视角出发,聊聊 Playwright 相对 Selenium 的核心优势,包括自动等待、浏览器上下文、网络拦截、Trace 调试等。顺带会给出一个可复用的登录流程示例和一份迁移避坑清单。适合正在做 Web 自动化、或者准备重构现有 Selenium 脚本的工程团队参考。

1. 为什么测试开发都在聊 Playwright?——先看清痛点

1.1 Selenium 陪伴多年,但痛点越来越明显

Selenium WebDriver 是很多团队 Web UI 自动化的起点,它通过 WebDriver 协议和浏览器通信,确实解决了跨浏览器的问题。但用久了你会发现,真正耗时间的不是写用例本身,而是“伺候稳定性”。

第一个痛点就是等待策略。Selenium 里要把隐式等待、显式等待、强制等待调教好,得靠经验堆。WebDriverWait虽然强大,但你要精准判断一个元素什么时候可点击、什么时候可见、什么时候在 DOM 中,写出来的代码又长又容易踩边界问题。更麻烦的是,隐式等待和显式等待混用时,可能会产生奇怪的超时行为。

第二个痛点是对浏览器的操作粒度太粗。想开两个独立的登录会话,你得手工管理多个 driver 实例;想访问新开的标签页,得用driver.switch_to.window去处理window_handles;想模拟手机端,那又得套一层繁琐的 Options 配置。这些操作不是不能完成,而是每一次都让你觉得在跟浏览器的内部机制搏斗。

第三个痛点是网络层完全缺失。Selenium 定位的是“表现层自动化”,它天生不擅长捕获请求、mock 接口返回、模拟弱网。如果你要做接口联调和故障注入,通常只能借助 BrowserMob 之类的代理工具,或者直接在服务端造数据。测试环境不干净,用例就只能飘。

1.2 Playwright 的定位与“开箱即用”思路

Playwright 是一个新兴的开源 Web 自动化测试框架,核心思路是“用现代浏览器的能力,重新定义 UI 自动化”。它没有沿用 Selenium 的 WebDriver 协议,而是通过浏览器开发工具协议直接和浏览器深度对话。这意味着等待、导航、点击、输入、网络、权限等操作,都变成了第一等公民。

我最早接触 Playwright 的时候,最直观的感受是:它像是一个“自动化打包好的浏览器”,而不是一个“遥控器”。你不需要为了实现一个弹窗监听去翻各种 hack,因为框架本身就把浏览器能力全部暴露出来了。单独一个标签页、多页面上下文、移动设备模拟、网络拦截、下载事件、对话框监听,这些在 Selenium 里要么做得很吃力,要么干脆不支持。

另一个容易被忽略的点是,Playwright 把“测试隔离”作为设计基础。每个BrowserContext都是独立的会话,类似匿名浏览环境。这正好命中测试开发最关心的“用例互不污染”。后面我会专门展开说这一点。

2. Playwright 核心优势拆解:从 API 到运行机制

2.1 自动等待机制:实测最省心的一点

Selenium 里常见的三段式写法:

element = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "submit")) ) element.click()

Playwright 里同一件事就一句话:

page.click("#submit")

不是 Playwright 偷懒,而是它的内部实现了“动作前自动等待”。执行点击之前,Playwright 会自动检查元素是否附着在 DOM、是否可见、是否稳定、是否接收事件。如果条件不满足,它会等待一段时间,默认超时时间是 30 秒。等待期间它还会持续重试,直到元素达到可以接收操作的状态。

这意味着你不需要再为“测试环境慢、接口返回慢、字体加载慢”写一堆time.sleep(5)。我之前维护过一个老系统,Selenium 脚本里有十几个sleep,改到 Playwright 之后全部删掉,总执行时间反而缩短了将近 40%。

不过要提醒一点:自动等待不是万能药。它有默认的“可操作状态”判定,但如果你点击的元素被透明遮罩挡住,或者你自己写的自定义组件状态比较特殊,仍然需要根据实际场景做一些针对性处理。至少,大部分因为等待造成的 flaky 用例,在 Playwright 里会明显减少。

2.2 浏览器上下文与多标签隔离:并行测试的解药

Selenium 里想模拟两个独立用户同时登录,要么启动两个 driver 进程,要么想办法在两个窗口之间反复切换,数据隔离很难做干净。Playwright 给出了一个更优雅的抽象:BrowserContext。

每个BrowserContext就是一个完全隔离的会话,相当于一个独立的浏览器实例。不同 context 之间共享浏览器内核,但 Cookie、缓存、本地存储、权限设置完全隔离。你在一个 context 里登录了账号 A,在另一个 context 里做未登录操作,两边互不影响。

代码长这样:

with sync_playwright() as p: browser = p.chromium.launch(headless=True) ctx_user_a = browser.new_context() ctx_user_b = browser.new_context() page_a = ctx_user_a.new_page() page_b = ctx_user_b.new_page()

这个特性对做多角色权限测试特别有用。以前我要反复退出登录、重新登录,现在直接开两个 context,两边同步操作,中间还能加上断言。

再进一步,context还能直接打开多个标签页。在 Selenium 里处理window_handles真的很容易写乱,Playwright 则把每个页面当成独立的Page对象,访问新标签页只需要context.wait_for_event("page")或者直接context.pages遍历。代码可读性和维护性完全不是一个数量级。

移动端模拟也内建在context里,你可以直接把 UA、viewport、设备缩放参数传进去:

ctx = browser.new_context( **p.devices["iPhone 13"] )

这套玩法在 Selenium 里要配置一堆 Options,而且经常踩版本兼容的坑。Playwright 就是把“贴近真实用户环境”这件事标准化了。

2.3 拦截网络请求与响应:接口校验和 Mock 一把梭

UI 自动化最头疼的场景之一是:页面行为依赖后端接口,而后端接口还不稳定。Selenium 在这个问题上基本无能为力,Playwright 则把网络拦截做成了原生能力。

page.route可以拦截所有 HTTP 请求,然后选择继续请求、直接返回假数据、或者中止请求。我用得最多的场景是“接口 pending 状态”的 UI 验证:

def handle_route(route): if "api/order" in route.request.url: route.fulfill( status=200, content_type="application/json", body='{"code": 0, "data": []}' ) else: route.continue_() page.route("**/*", handle_route)

这样不需要等服务端真的返回订单列表,你就可以构造“空列表”“异常数据”“超大列表”等用例来验证前端展示逻辑。

你还可以直接监听接口响应,用来做断言。比如提交表单后,确认某个埋点请求是否触发:

with page.expect_request("**/analytics/submit") as req: page.click("#submit") assert req.method == "POST"

这种能力在 Selenium 生态里几乎不可想象。它让 UI 自动化真正具备了“白盒”能力,不再只是在黑盒上戳按钮。

3. 实操落地:用 Playwright 重写一个典型登录流程

3.1 前置准备与安装

Playwright 支持 Python、JavaScript/TypeScript、Java、.NET,测试开发团队用 Python 的最多,我下面以 Python 为例。

安装非常直接:

pip install playwright playwright install chromium

playwright install会把对应浏览器下载到本地缓存目录,注意它不依赖你系统里的 Chrome,内置的浏览器版本是独立管理的。这样做的好处是环境一致,不会因为本机和 CI 的 Chrome 版本不同导致行为差异。

还有一个常见的坑是:公司在内网环境,pip 源和浏览器下载都受限。这时候需要配置镜像环境变量,或者提前把浏览器缓存打进镜像。如果你们有统一的 CI 镜像,建议在镜像构建阶段就执行playwright install,别等跑测试时临时现下。

3.2 核心脚本实现与逐步解读

下面是一个典型的登录流程,加上了断言和失败截图:

from playwright.sync_api import sync_playwright def test_login(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context( viewport={"width": 1280, "height": 720} ) page = context.new_page() page.goto("https://example.com/login") page.fill("#username", "tester01") page.fill("#password", "Passw0rd!") page.click("button[type=submit]") page.wait_for_selector(".welcome", timeout=10_000) assert page.locator(".welcome").inner_text() == "欢迎回来" context.storage_state(path="login_state.json") browser.close()

逐步拆解一下:

  • browser.new_context创建隔离会话。如果你要做“记住我”之类的操作,可以把 context 的登录态存成storage_state,下次测试里直接复用,省去每次都扫码或者走验证码。

  • page.fill会自动清空输入框再填入内容,比 Selenium 的send_keys更符合用户行为。如果输入框有字符数限制,也能更早发现问题。

  • page.click("button[type=submit]")触发前内部会检查按钮是否可点击。如果按钮在页面底部被遮挡,Playwright 会自动滚动到可见区域。

  • wait_for_selector是显式等待,但比 Selenium 的WebDriverWait简洁得多。你也可以用expect断言的写法,配合 pytest 的插件会更顺手。

这个脚本跑完后,会生成login_state.json。它在文件里记录了当前会话的 Cookie、localStorage、IndexedDB 状态。其他测试里加载这个文件,就可以直接跳过重复登录:

context = browser.new_context(storage_state="login_state.json")

当然,登录态有失效时间,过期之后需要重新走一次登录。建议在测试数据准备阶段自动判断是否需要重建登录态,而不是每次都硬等。

3.3 Trace 与录屏:排查问题比 Selenium 舒服太多

Selenium 时代排查失败用例,最常用的方式是截图。截图只能看到最终画面,不知道中间发生了什么。Playwright 提供了 Trace 文件,会把整个会话里的 DOM 快照、网络请求、控制台日志、页面操作过程全部记录下来。

开启方式很简单:

context.tracing.start(screenshots=True, snapshots=True, sources=True) # 运行测试... context.tracing.stop(path="trace.zip")

跑完后用playwright show-trace trace.zip打开 viewer,你可以看到每一步操作前后的页面状态,还能直接展开某个时间点的 DOM 树。这比对着失败日志猜原因高效太多了,尤其是处理线上环境“偶现失败”的问题。

录屏也是内置能力:

context = browser.new_context(record_video_dir="videos/")

不过录屏文件会占用不少存储,不适合每个用例都开。我一般只针对“冒烟用例关键链路”或者“定位到可疑用例”时临时开启。Trace 同理,不建议全量开启,只在 CI 上对失败用例自动收集是比较划算的方案。

4. 常见问题与踩坑实录

4.1 元素定位到了还是操作失败?

Playwright 的自动等待虽然强,但偶尔会遇到“定位到了,点击还是失败”的情况。最常见的原因是元素外层的遮罩层。我刚迁移的时候,在一个弹窗关闭按钮上持续踩坑。按钮明明可见,但父容器有一个透明遮罩挡在前面,Playwright 判断该元素“不可接收事件”,于是重试到超时。

解决思路有两个。一个是等待遮罩消失:

page.locator(".mask").wait_for(state="hidden")

另一个是强制点击,但这不是首选:

page.locator(".close-btn").click(force=True)

force=True会跳过可操作性检查,直接触发点击。它能解决绝大多数遮罩问题,但也会掩盖某些真实交互问题,使用前要评估一下。

还有一个细节:页面里有多个匹配相同选择器的元素时,page.click会直接报 strict mode violation。这是 Playwright 刻意设计的,为了让你意识到选择器不唯一。不要一上来就加.first(),而应该回头检查元素的属性结构,想办法选出更稳定的目标。

4.2 数据驱动测试时如何管理浏览器与上下文

很多测试框架都喜欢在 fixture 里直接定义一个全局的 browser,然后把同一个 page 传给所有用例。在 Playwright 里这不是最优解。因为同一个 context 会让用例共享 cookie 和缓存,失去了隔离意义。

我踩过几次之后,总结出的模式是:测试函数级创建新 context,测试模块级创建新 page 也行,但必须保证 context 是新的。尤其是用例里会修改登录态、权限配置的操作,一个 context 只服务一个用例,用完即弃,稳定性才能上来。

用 pytest 的话,可以写一个简单的 fixture:

@pytest.fixture def page(): with sync_playwright() as p: browser = p.chromium.launch() context = browser.new_context() page = context.new_page() yield page browser.close()

如果你要跑大批量数据驱动用例,建议把浏览器实例复用,context 大批量创建,而不是每个用例都启动一个新浏览器。启动浏览器的开销比新建 context 大得多,这个优化往往能让整轮测试缩短 30% 以上。

4.3 从 Selenium 工程迁移到 Playwright 的注意点

很多团队不是从零开始,而是想把已有 Selenium 脚本搬过来。我建议别想着“一行行翻译”,而是先理解旧脚本里哪些是为了规避 Selenium 缺陷写的 workaround,这些在 Playwright 里可以直接删掉。

维度SeleniumPlaywright
等待隐式等待 + 显式等待混用,容易超时自动等待,动作前校验可操作性
多标签页switch_to.window管理 handles每个 Page 独立,context 统一管理
会话隔离多个 driver 实例BrowserContext 天然隔离
网络拦截依赖外部代理工具原生route/expect_request
调试截图 + 日志Trace 回放 + DOM 快照 + 录屏
移动端手写大量参数p.devices一键模拟
并行测试容易相互干扰context 配合 worker 进程,生态较完善

迁移时最重要的一个心态是:先别追求把所有用例搬过去,而是挑一个高频且容易 flaky 的模块做试点。我当时先选了支付流程,因为它的业务状态复杂,旧框架下用例三天两头失败。新版跑稳后再逐步推广。

常见迁移报错里,要注意几点:

  • Selenium 里习惯用 CSS 类名拼接定位,比如.btn.active,Playwright 支持这种选择器,但更建议用>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询