做 Web 自动化的人,最近几年应该都反复听到同一个说法:Selenium 已经过时了,Playwright 才是未来。我最早听到这句话时很不以为然,毕竟 Selenium 在测试框架里的地位,相当于老牌框架里的常青树,生态成熟、资料齐全、招人也好招。真正动手把 Playwright 用进一个真实项目之后,我才理解了这句话的底气来源。
不是 Selenium 突然变得不能用,而是 Playwright 解决的已经不是同一层问题。Selenium 解决的是“能跑”,Playwright 解决的是“能稳定跑、能批量跑、能长期维护着跑”。这个区别看起来很微小,实际进入项目后,你会发现它决定了团队能不能把 UI 自动化真正用起来,而不是写完就烂在仓库里。
这篇文章不打算做框架之间的全面测评,也不会把官方文档搬一遍。我更想从一个实际使用者的角度,讲清楚 Playwright 和 Selenium 的差异到底在哪里、Playwright 的录制脚本能不能直接用于生产、以及把它落地到真实项目时最容易踩的坑。如果你正在纠结要不要迁移,或者刚接触 Playwright 想判断这东西到底值不值得学,这篇文章应该能给你一个比较完整的参考。
1. 先搞清楚“Selenium 退休论”到底在说什么
1.1 Selenium 的成功建立在它解决了“浏览器驱动不一致”这件事上
要理解为什么会有“Selenium 推出历史舞台”这种说法,先往回看一下。
Selenium 在很长一段时间里几乎是 Web 自动化的代名词。它最核心的价值,不是提供了多少 API,而是把不同浏览器的操作统一成了一套标准接口。以前你写自动化测试,要考虑不同浏览器的操作差异,Selenium 帮你把这一层屏蔽掉了。这是它在那个年代最了不起的贡献。
但这也带来了一个副作用:它解决问题的方式,是在浏览器外面加一层驱动协议。你的测试脚本通过 WebDriver 协议和一个独立的浏览器驱动进程通信,驱动再去操作浏览器。这样一个架构天然会有几个麻烦:
- 每个浏览器都要单独下载对应版本的驱动,而且浏览器一升级,驱动版本可能就失效。
- 脚本和浏览器之间的通信要经过一个中间层,理论上步骤越多,出问题的概率越高。
- Selenium 对点击、等待、元素状态的处理,整体还是偏“手工”。你需要在代码里显式写等待,显式判断元素是否可见、可点击,否则脚本就容易在速度波动时挂掉。
这些不是 Selenium 的缺陷,而是那个时代的技术取舍。只要浏览器没有提供更底层的自动化协议,这套方案就是最可行的。问题在于,后来浏览器真的提供了更底层的协议。
1.2 Playwright 真正变的不只是 API,而是通信层
Playwright 对很多人来说,第一印象是“写起来更简洁”。但其实表面的 API 简洁只是结果,真正的变化发生在通信层。
Playwright 大部分浏览器自动化操作,走的是浏览器开发者工具协议(CDP)或者在 Firefox 上对应的调试协议。它不再像 Selenium 那样,通过独立驱动进程解释命令,而是直接和浏览器内部的调试能力对话。这带来的实际好处非常明显:
- 安装 Playwright 时,可以选择用官方命令直接下载配套浏览器,不需要像 Selenium 那样手动匹配驱动版本。
- 很多操作可以等待条件满足后再继续,而不是机械地 sleep 固定秒数。
- 对页面内部事件、网络请求、控制台日志的感知能力更强。
用一句话概括:Selenium 是站在浏览器门口遥控指挥,Playwright 更像是直接坐进了浏览器的驾驶舱。指挥方式再熟练,也不如直接握着方向盘来得顺手。
所以“Selenium 退出历史舞台”这个说法,如果理解为“Selenium 马上没人用了”,那并不准确。现有项目、老团队、大量历史脚本,都还在稳定运行。更准确的说法是:从新项目选型和新手入门的视角看,Playwright 已经是更合理的默认选择。
2. Playwright 的核心机制和 Selenium 有什么本质差异
2.1 自动等待机制:告别随机 sleep 才是最省心的变化
Selenium 项目里最常见的代码片段是什么?time.sleep(3)或者WebDriverWait。前者是写的人没耐心,后者是写的人有经验但也得手动指定等待条件。
Playwright 默认的行动机制不是这样的。它的大多数操作,比如点击、填表、截图,会先自动检查元素的可见、稳定和可操作状态,满足条件后再执行操作。也就是说,你不用每次点击前都去判断“这个按钮是不是已经加载出来了”。
这一点表面看只是省几行代码,实际影响非常大。真实项目里页面加载速度不稳定,网络抖动频繁。手动等待写少了,脚本会偶发失败;写多了,整体运行时间又会被拖得很长。Playwright 的做法是让操作本身具备“等到能操作再操作”的能力,这让脚本的稳定性上限一下子提高了。
不过强调一下,自动等待不是玄学。它是在合理的超时范围内等待元素达到可操作状态。如果页面本身有脚本报错,或者异步请求一直不返回,该失败还是会失败。它的最大价值是减少那些“本来没问题,只是加载慢了一点点”的随机失败。
2.2 定位器模型:比找元素更接近人的操作习惯
用过 Selenium 的人通常很熟悉find_element_by_id、find_element_by_xpath这类写法。Playwright 也有对应的定位方式,但它更推荐使用 Locator 这个概念。
两者看起来都是“找到元素再做操作”,但实际工作方式差别很大。
Selenium 是拿到一个元素引用,后续操作都基于这个引用。页面一刷新、异步组件一更新,这个引用可能就失效了,需要重新查找。Playwright 的 Locator 则更像一个“定位规则”。你在规则里写“找到页面里的登录按钮”,它会在操作发生的时刻重新去匹配这个规则。只要页面里仍然有满足条件的元素,操作就能继续。
我用一个生活里的例子解释:Selenium 的做法是先在地图上钉一个图钉,然后按图钉找路;Playwright 的做法是记住地址,每次到地方再按门牌号找。前者在路况不变时很好用,后者在页面频繁变化时更稳。
尤其是当你需要处理动态列表、弹窗、异步渲染的场景时,Locator 这种“按规则重新查找”的方式,会明显减少因为元素引用过期导致的报错。
2.3 上下文隔离:一个测试用例一个独立环境
Selenium 打开一个浏览器窗口,多个测试用例如果共用这个窗口,状态很容易串。你登录了 A 账号,下一个用例可能就被带进了登录态,最后用例之间相互影响,定位问题非常痛苦。
Playwright 引入了 Browser Context 的概念。每个 Context 相当于一个全新的浏览器会话,Cookie、LocalStorage、缓存都是独立的。你可以在同一个浏览器实例里创建多个 Context,每个测试用一个新的 Context,这样就模拟出了完全不相关的用户访问。
这意味着什么?意味着你不需要在测试之间去清缓存、清 Cookie、重置本地数据。每个用例天然拥有一个干净的浏览器环境。这在 Selenium 里不是无法做到,而是你需要额外做一堆清理逻辑。Playwright 是在架构层面把这个事解决了。
3. 一个最小可运行的 Playwright 项目怎么搭起来
说再多机制,不如真正上手跑一次。这里我给出一个最小可运行的 Playwright 项目结构,适合第一次接触的人照着走。
3.1 安装和环境准备
Playwright 支持 Python 和 Node.js 两套生态。我这里以 Python 为例讲,因为目前国内很多做测试自动化的团队是 Python 技术栈。
安装分两步:
pip install playwright playwright install chromium第一行是安装 Playwright 库,第二行是下载 Playwright 维护的 Chromium 浏览器。注意,这个浏览器是和 Playwright 测试过兼容性的,和你本机日常用的 Chrome 可以共存。这样做的最大好处是,你不需要手动去下载 ChromeDriver,也不需要担心浏览器自动更新后驱动版本对不上。
如果你的项目还需要跑 Firefox 或 WebKit,也可以单独装:
playwright install firefox playwright install webkit但正式的自动化项目,建议先锁定一种浏览器跑通流程。浏览器多不是问题,问题是你得维护多套兼容性。
3.2 录制脚本:先用 Codegen 生成第一版
Playwright 里最适合入门的不是写代码,而是直接录制。
执行命令:
playwright codegen会自动打开一个浏览器窗口,同时弹出一个脚本生成面板。你在浏览器里的每一步操作——点击、输入、跳转、选择——都会自动翻译成 Playwright 代码。操作结束后,把生成的代码复制到项目里,就拿到了一个能跑的基础脚本。
有一个关键点必须提醒:录制生成的代码,不等于可以无脑上线。
Codegen 适合用来快速了解目标页面的操作路径、生成最原始的流程框架,但生成代码通常需要你后续重新整理。比如它会把某个输入框的定位器写得非常具体,页面结构稍一改就失效;再比如它生成的代码不会有明确的业务断言,你不知道这次操作到底成功没有;又比如它不会主动处理登录态、验证码、动态数据这类场景。
所以我的建议是:把录制当成“看答案”的工具,不要把它当成“写脚本”的工具。录制的作用是让你快速摸清页面结构,真正的脚本还是要在项目里一步步完善。
3.3 第一个稳定的登录脚本
我这里给一个很常见的例子:打开一个测试站点,完成一次登录,然后断言登录结果。代码写法如下:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() page.goto("https://example.com/login") page.get_by_label("用户名").fill("testuser") page.get_by_label("密码").fill("testpass") page.get_by_role("button", name="登录").click() # 断言登录成功,比如登录后会出现用户的头像 page.get_by_alt_text("用户头像").wait_for(state="visible") browser.close()这段代码里的几个细节值得说:
get_by_label会优先按 label 文本定位表单控件,比 xpath 可读性好很多。get_by_role按按钮的语义角色和可访问名称定位,符合真实用户理解页面的方式。wait_for(state="visible")是显式等待一个元素可见,配合默认自动等待,大多数情况下已经够用。
如果你想模拟真实的登录场景,可以改用headless=True放在服务器或 CI 里跑,但注意,headless 模式下页面表现和肉眼所见有差异,首次落地时建议先有头模式跑几遍,确认步骤稳妥后再切 headless。
4. 定位元素是 Playwright 里最容易混淆的一环
搜索材料里出现了很多和 Playwright 定位相关的问题,比如怎么定位span、怎么定位动态 iframe、怎么处理 shadow DOM。定位方式直接决定脚本的稳定性,值得单独展开。
4.1 从最朴素的 id 到角色定位,优先级怎么选
很多 Selenium 老手刚转 Playwright,第一反应还是找 id 或 xpath。这也不是不行,但 Playwright 的定位器可以更贴近人的理解方式。
以点击一个按钮为例:
page.click("#submit_btn") # 按 id 定位 page.locator("button.submit").click() # 按 CSS 定位 page.get_by_role("button", name="提交").click() # 按角色和可访问名称定位我更推荐优先使用get_by_role、get_by_text、get_by_placeholder这类语义化定位方式。为什么?因为它们更接近真实用户对页面的感知,页面结构调整时,这类定位器往往还保持有效。比如按钮从一级菜单移到了二级菜单,只要可访问名称没变,脚本就不用改。
对于span这类问题,定位的核心不是标签名,而是元素的语义。你可以用page.locator("span:has-text('某个文案')")这样去匹配包含文本的 span,但更好的做法是看它有没有 role、data-testid 这类稳定标识。建议团队在开发阶段就约定好可测试属性,给关键交互元素加上稳定的>frame = page.frame_locator("#modal-frame") frame.get_by_role("button", name="确认").click()
这里要注意:如果页面里有多个 iframe,或者 iframe 是嵌套的,最简单的方式还是给 iframe 设置稳定的 id 或 name,定位才不容易乱。
Shadow DOM 是另一个坑。很多现代前端框架会使用 Shadow DOM 封装组件内部结构,外部普通选择器碰不到它。Playwright 的 CSS 定位方式会穿透部分 shadow 边界,但为了稳妥,我通常建议:
- 优先寻找组件暴露出来的公共属性。
- 如果只能穿透 shadow DOM,用
page.locator("css=selector")并仔细验证。 - 实在不行,退回组件外层容器,用文本或相对位置定位。
动态列表是异步渲染最常见的场景。录制的脚本很容易在这种地方失效,因为录制时列表已经渲染好了,而实际运行时网络慢一点,列表还没出现,脚本就去找第二行、第三行的内容。
处理方式很简单:先定位列表容器,再等待列表项数量符合预期。
rows = page.locator("table tbody tr") rows.nth(0).wait_for(state="visible") # 再开始操作 rows.nth(1)核心原则是:不要假设元素已经存在,要把它当作一个“等它出现”的异步过程。
5. 录制生成的脚本为什么不能直接上线
5.1 缺少业务断言,失败了你根本不知道
Codegen 录制出来的脚本,本质是一份“操作记录”。它记录了你要在页面上做什么,但它不关心结果对不对。
举个很常见的场景:你录制了登录三步操作,输入账号、输入密码、点击登录。如果登录失败,页面上出现一个红色错误提示,Codegen 生成的脚本完全不知道这算失败。它只会按照录好的流程继续往后走,然后在找不到下一个元素时报错。
你依然会看到测试失败,但失败原因被推后到了下一个环节,而不是失败发生的真正位置。这个问题在真实项目中会浪费大量排查时间。
正确的做法是,在关键操作后面加断言:
page.get_by_text("登录成功").wait_for(state="visible")或者:
assert page.title() == "控制台"录制生成的脚本,作用只是“快速建立流程骨架”,断言、异常处理、数据清洗,这些业务逻辑必须自己补。
5.2 只在一种网络条件下稳定,换个环境就失败
录制过程中,页面加载速度、图片资源、字体、接口响应时间都和你录制当时的环境有关。录制跑的慢或者快,都不会影响生成脚本本身。但问题在于,脚本里如果隐含着“加载速度恒定”的假设——比如某些地方依赖了隐式等待或者快速点击——拿到另一个环境时就会暴露成不稳定。
解决思路有三个层面:
- 优先利用 Playwright 的自动等待,不要在点击后手动加过长的等待。
- 不要把选择器写得太长,尽量找到稳定、短小、语义清晰的定位器。
- 把真实环境配置参数化,比如测试地址、测试账号、等待超时,都放到配置文件里。
录制只是种子,不是成品。这一点如果你能尽早想通,后面维护成本会低非常多。
5.3 验证码、登录态和测试数据,录制通通管不了
真实项目里最难自动化的不是点击,而是登录态。常见做法是用 storage state 来复用已登录状态。
你可以先在有头模式下人工登录一次目标站点,然后通过 Playwright 把当前 Context 的存储状态保存下来:
context.storage_state(path="./login_state.json")之后的脚本每次启动时直接用这个状态:
context = browser.new_context(storage_state="./login_state.json")这样就能做到不重复走登录流程。需要的注意点有两个:
- 登录态文件不能提交到公开仓库,里面包含敏感信息。
- 这种状态会过期,过期后需要重新人工登录生成一次。
验证码这类场景,录制更无法处理。如果项目里的验证码不是为了对抗自动化,而是为了防控机器人,那么最好和相关产品同学沟通,为测试环境提供万能验证码或直接关闭验证码。这是效率最高的路径。如果只能在线上环境跑,那就需要接入专门的识别方案,但这就超出了框架层面能讨论的范畴,而且容易涉及灰色手段,不建议直接写在工程流程里。
6. 从单条脚本到工程化,还差这几块能力
6.1 错误重试和不稳定用例处理
UI 自动化最经典的问题是:今天能跑过,明天跑不过,后天又能跑过。即使 Playwright 的自动等待已经解决了很多问题,网络、第三方登录框、服务端抖动仍然存在。
所以工程上必须给用例加上重试机制。pytest 生态里可以使用pytest-rerunfailures,或者自己写重试逻辑。一般的做法是:用例第一次失败时,先截图和收集页面日志,然后清空浏览器上下文重新跑一次。如果第二次通过,可视为“可重试的偶发失败”;如果连续失败两次,就应该报告为真实失败。
这里有一个细节:不要把重试当成遮羞布。如果某个用例频繁触发重试,说明它的定位器不够稳定,或者流程依赖了不稳定因素。重试是提升自动化最终通过率的最后一道保险,而不是让你忽略脚本问题的借口。
6.2 失败现场保留:截图、视频和 trace
Playwright 在失败排查上有几个杀手级能力:
- 失败时自动截图,保存当前页面。
- 录制浏览器操作视频,复盘整个执行过程。
- 生成 trace 文件,里面包含每个步骤的 DOM 快照、网络请求、控制台日志和操作时间。
在一个真实项目里,视频和 trace 的价值极高。因为 UI 自动化最耗时的事情不是写脚本,而是排查“它到底为什么失败”。没有现场信息时,你只能一个个打印日志去猜。有了 trace,你可以直接回放那个时刻的页面状态,连当时的控制台报错都能看到。
建议在 fixture 或 hooks 里统一处理失败现场,比如在 pytest 的 conftest 中配置:
@pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() if rep.when == "call" and rep.failed: # 获取当前 page 对象,截图和保存 trace pass不用把每个用例都写成带一堆 try-except 的结构。统一放在框架层处理,才能在项目大了以后保持可维护性。
6.3 批量任务和并发执行
很多人关心 Playwright 能不能并发执行。这个问题要分两层看。
第一层,Playwright 本身支持在一个浏览器进程中创建多个页面或上下文,但并行能力最终取决于机器资源。如果你要同时跑 10 个用例,建议给足够的 CPU 和内存,否则每个浏览器实例都会拖慢其他实例。
第二层,如果只是接口层面的自动化,Playwright 不是最优选择。UI 自动化的接口数据准备、页面返回桩数据,最好用专门的接口测试方案。Playwright 更合适的定位,是验证用户真实操作路径完整跑通。
所以在实践中,我们通常把用例分成两类:
- 冒烟测试:核心流程,每次发版前必须跑完,并发度高,耗时短。
- 回归测试:全量用例,跑在夜间构建里,允许时间长,重点看趋势是否稳定。
这里建议所有并发策略先从小规模开始。先用 2 个并发跑一批用例,观察资源占用情况,再逐步增加。不要一上来就 20 个并发,否则最后很难判断是业务 bug 还是机器资源不够。
6.4 接入 CI 之后要注意的细节
在 CI 上跑 Playwright 和本机跑,最大的差异有两个。
第一是浏览器依赖。CI 机器通常比较干净,需要先执行playwright install --with-deps安装系统级依赖。不装会直接遇到缺失系统库的报错。
第二是无头模式。日常开发建议有头模式便于观察,CI 建议无头模式提高稳定性。如果你遇到“本机能过、CI 不能过”的问题,不要先怀疑 Playwright,而是优先检查:
- 是否依赖了本机存在的 bharti 字体、系统路径等环境因素。
- 是否使用了当前时间、随机数这类不稳定测试数据。
- 是否有跨用例共享的全局状态。
排查顺序很重要:先看输入数据和测试数据,再看运行环境,然后再怀疑框架本身。这个顺序能帮你少走很多弯路。
7. 什么场景继续用 Selenium,什么场景应该换 Playwright
7.1 还适合继续用 Selenium 的情况
虽然我整体推荐在新项目里用 Playwright,但依然存在一些场景,Selenium 是合理的选择:
- 现有项目已经有大量运行良好的 Selenium 脚本,迁移成本远高于维护成本时。
- 团队所有人对 Selenium 非常熟悉,且短期没有精力做技术切换时。
- 项目里有某些特定浏览器或特定版本驱动,Playwright 暂时覆盖不到。
- 整条链路里大量依赖 Selenium Grid 或其他选型已固定的平台组件。
技术选型最怕的不是“旧”,而是“不适合团队现状的折腾”。如果现有 Selenium 方案稳定、能跑、能维护,就不必为了追新而硬换。
7.2 适合切到 Playwright 的信号
反过来,如果你满足下面任意两条,我建议认真评估 Playwright:
- 新项目起步,还没有历史脚本包袱。
- 团队正在为大量随机 sleep 和 WebDriverWait 代码头疼。
- 经常遇到浏览器驱动版本和浏览器版本不匹配的问题。
- 需要录制脚本快速生成原型,帮助测试人员快速上手。
- 需要在复杂 iframe、多页面、多标签、多用户场景下做稳定自动化。
- 想要在 CI 上获得更好的并发能力和失败现场还原能力。
Playwright 不是一个完美的工具,但是它的工程化能力和开发者体验,放在今天的 Web 环境下,确实更符合现代 Web 应用的真实情况。
7.3 写久了会发现,工具从来不是唯一难点
最后聊一点稍微宏观的体会。
很多人在选型时会陷入一个误区:总觉得换一个框架就能解决 UI 自动化不稳定、维护成本高的问题。换框架确实会带来短期的新鲜感和一些效率提升,但长期看,决定 UI 自动化项目成败的因素其实和框架关系不大,更关键的是:
- 被测试系统本身是否稳定,测试环境是否有独立的测试账号和测试数据。
- 团队是否愿意维护测试代码的质量,而不是“能跑一次就行”。
- 失败信息是否足够完整,让你能快速判断是环境问题、数据问题还是代码问题。
- 自动化脚本是否只覆盖真正值得覆盖的核心流程,而不是试图用 UI 模拟一切。
Playwright 能帮你的是:少写等待代码、得到更清晰的失败现场、让录制和调试更顺畅。但它不能帮你解决测试数据混乱、环境不稳定、业务逻辑总在变的问题。
我的判断是:新项目选 Playwright 是对的,老项目如果 Selenium 跑得稳,也不急着动。真正需要用框架更新的地方,往往不是“工具太旧”,而是“流程太脆弱”。把稳定性、可维护性、可观测性做好,哪怕你用 Selenium,也能做出高质量自动化;反过来,如果只换框架不改造流程,换谁都是一样会烂。
所以,比起纠结哪个框架退出历史舞台,更值得你花时间想清楚的,是你要用自动化解决什么问题、怎么保证它持续可用,以及面对失败时你的排查路径够不够短。Playwright 是我现在更愿意用的工具,但它不是万能解,它只是让整个工作流更接近“可控”的那个关键环节。