ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Playwright vs Selenium:Web自动化测试框架选型与工程化实践

Playwright vs Selenium:Web自动化测试框架选型与工程化实践 做 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(headlessFalse) 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(statevisible) browser.close()这段代码里的几个细节值得说get_by_label会优先按 label 文本定位表单控件比 xpath 可读性好很多。get_by_role按按钮的语义角色和可访问名称定位符合真实用户理解页面的方式。wait_for(statevisible)是显式等待一个元素可见配合默认自动等待大多数情况下已经够用。如果你想模拟真实的登录场景可以改用headlessTrue放在服务器或 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(cssselector)并仔细验证。实在不行退回组件外层容器用文本或相对位置定位。动态列表是异步渲染最常见的场景。录制的脚本很容易在这种地方失效因为录制时列表已经渲染好了而实际运行时网络慢一点列表还没出现脚本就去找第二行、第三行的内容。处理方式很简单先定位列表容器再等待列表项数量符合预期。rows page.locator(table tbody tr) rows.nth(0).wait_for(statevisible) # 再开始操作 rows.nth(1)核心原则是不要假设元素已经存在要把它当作一个“等它出现”的异步过程。5. 录制生成的脚本为什么不能直接上线5.1 缺少业务断言失败了你根本不知道Codegen 录制出来的脚本本质是一份“操作记录”。它记录了你要在页面上做什么但它不关心结果对不对。举个很常见的场景你录制了登录三步操作输入账号、输入密码、点击登录。如果登录失败页面上出现一个红色错误提示Codegen 生成的脚本完全不知道这算失败。它只会按照录好的流程继续往后走然后在找不到下一个元素时报错。你依然会看到测试失败但失败原因被推后到了下一个环节而不是失败发生的真正位置。这个问题在真实项目中会浪费大量排查时间。正确的做法是在关键操作后面加断言page.get_by_text(登录成功).wait_for(statevisible)或者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 失败现场保留截图、视频和 tracePlaywright 在失败排查上有几个杀手级能力失败时自动截图保存当前页面。录制浏览器操作视频复盘整个执行过程。生成 trace 文件里面包含每个步骤的 DOM 快照、网络请求、控制台日志和操作时间。在一个真实项目里视频和 trace 的价值极高。因为 UI 自动化最耗时的事情不是写脚本而是排查“它到底为什么失败”。没有现场信息时你只能一个个打印日志去猜。有了 trace你可以直接回放那个时刻的页面状态连当时的控制台报错都能看到。建议在 fixture 或 hooks 里统一处理失败现场比如在 pytest 的 conftest 中配置pytest.hookimpl(tryfirstTrue, hookwrapperTrue) 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什么场景应该换 Playwright7.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 是我现在更愿意用的工具但它不是万能解它只是让整个工作流更接近“可控”的那个关键环节。
返回列表