ARTICLE DETAIL

资讯详情

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

从Selenium到Playwright:UI测试的范式转移与实战指南

从Selenium到Playwright:UI测试的范式转移与实战指南 如果你这几年一直在用 Selenium 写 UI 测试应该会越来越觉得别扭全量用例跑一趟一半时间在等 sleep换个浏览器版本WebDriver 对不上就罢工想定位一个动态渲染的元素XPath 写出来自己都嫌弃。我不是说 Selenium 不行它确实培养了一整代测试开发工程师在 Web 自动化领域当了十几年事实标准。但 Playwright 出现之后我越来越倾向于认为这不只是换了个工具而是一次 UI 测试工具层面的范式转移——从浏览器外部操作到浏览器内部协作。这篇文章我不打算写成一堂工具使用课而是想以一个踩过 Selenium 各种坑、又用 Playwright 重构过多个项目的人的身份聊聊这两个工具背后的设计逻辑差异为什么 Playwright 能解决 Selenium 时代那些让人抓狂的问题以及你从旧体系迁移过来时最需要关注哪些细节。1. 为什么 Selenium 统治了那么久却被称为旧范式1.1 WebDriver 协议当年的功臣今天的瓶颈聊范式转移之前先得弄清楚 Selenium 当初是怎么工作的。Selenium 2.0 之后的核心是 WebDriver 协议说白了就是一套基于 HTTP 的接口约定。你的测试代码通过 HTTP 请求访问本地或远程的 WebDriver 服务再由这个服务把命令转成浏览器能理解的指令。每一步操作都是标准的请求—响应模式。这套架构在那个年代非常先进它用一套统一接口屏蔽了不同浏览器的差异Firefox 有 geckodriverChrome 有 chromedriverIE 还有一套让老测试工程师头皮发麻的 driver。你只要写一套测试代码理论上就能跑在不同浏览器上。这是 Selenium 能统治十多年的根本原因。但这个架构的瓶颈也很明显通信成本高、信息单向、反馈滞后。每一条命令都要走一次 HTTP 来回浏览器内部的渲染进度、网络状态、控制台日志Selenium 并不能主动感知。最典型的一个痛点就是等待——页面到底加载完没有Selenium 自己不知道只能靠测试代码里的time.sleep或者显式等待去反复探测。你可以把它理解成一个站在门外指挥的人每走一步都要敲门问一次屋子里什么情况他全靠猜。1.2 等待、定位、多分支Selenium 三大痛点实录我在 Selenium 时代写过不少能跑但丑得没法看的用例核心痛点逃不过这三大类第一是等待。理论上 Selenium 有隐式等待和显式等待但实际用起来非常考验经验。隐式等待一旦设置会对后续所有 find 操作生效包括那些根本不该等那么久的查询显式等待用起来又很啰嗦必须写WebDriverWait配合expected_conditions代码行数瞬间翻倍。更坑的是两者混用的时候等待时长会被叠加放大一个用例慢个十几秒是常事。到了后期团队里几乎形成了等不到就加 sleep的风气测试代码里全是time.sleep(3)跑起来奇慢无比。第二是元素定位。Selenium 里的find_element返回的是一个快照式引用只要页面发生了重排、刷新这个引用就可能失效你得重新查找。定位方式虽然全面但真正好用的不多XPath 写长了脆得一碰就碎CSS 选择器对动态 class 无能为力By.LINK_TEXT一遇到国际化就炸。最痛苦的还是 iframe 嵌套场景Selenium 要求你用switch_to.frame切进切出一旦嵌套层级深了代码里全是切换逻辑跟业务毫无关系。第三是浏览器上下文管理。多标签页、多窗口、弹窗、权限提示在 Selenium 里每一项都是状态管理问题。你要手动去window_handles列表里定位新窗口切过去用完还要切回来遇到 alert 弹窗还得用switch_to.alert处理。这些动作本质上都是在给浏览器当秘书帮它协调内部状态。可问题是浏览器本来就是多页面多上下文的状态集合体测试框架不应该逼着用户去管理这些底层细节。2. Playwright 到底改变了什么三个关键词2.1 CDP从浏览器外部到浏览器内部Playwright 的设计思路是反着来的——它不再通过一个外部 HTTP 接口发号施令而是尽可能钻到浏览器里面去工作。Chromium 系浏览器本身提供了 Chrome DevTools Protocol也就是 CDP 协议Playwright 直接跟这个协议打交道相当于坐在浏览器驾驶室里操作而不是站在车外喊话。这不是一个简单的协议替换它带来了一堆连锁红利。因为 CDP 本身能感知和干预浏览器内部几乎所有事件网络请求、DOM 变化、JavaScript 执行、控制台日志、性能指标甚至内存快照。所以 Playwright 从底子上就有能力做自动等待网络拦截追踪录制这些操作而不需要像 Selenium 那样靠轮询去猜。这里要澄清一个常见的误解Playwright 并不只支持 Chromium。它也支持 Firefox 和 WebKit对每个浏览器都封装了对应的驱动机制。但在日常使用中绝大多数团队都是优先跑 Chromium然后定期补跑其他内核因为速度更快、问题也更容易在本地复现。2.2 自动等待不用再写 sleep 和隐式等待Playwright 最让我舒服的一点就是默认的自动等待机制。你调用click、fill、check这类操作时Playwright 会先做一套可操作性检查元素是否可见、是否稳定不在抖动、是否已启用、是否可编辑、是否接收事件。如果条件不满足它会以轮询方式反复检查超时后才抛异常。这意味着什么意味着我写代码的时候基本不需要写显式等待更不用写 sleep。页面跳转有page.goto内部的等待元素出现有 locator 操作的自动等待请求完成有wait_for_load_state。这套机制把时序问题从测试代码里彻底拿掉了你只需要描述做什么不需要关心什么时候能做。我第一次用 Playwright 时拿一个老项目里的 200 条 Selenium 用例做对照实验同样的场景Selenium 版本平均每条用例跑 45 秒Playwright 版本平均 12 秒。这 33 秒的差距绝大部分就是省在了等待上。少了一堆轮询和 sleep用例反而更稳了因为自动等待是基于真实页面状态的而不是来自一个拍脑袋的固定延时。2.3 Locator一套选择器体系解决定位难题传统 Selenium 的定位方式可以概括为一次性查找而 Playwright 给你的是一个持续有效的定位器模型。用官方术语叫 Locator。这个对象本身是懒加载的它不立刻去页面里找元素而是在每次操作时重新查找。这带来的好处非常直接页面重渲染了、元素位置变了你的定位器依然有效不用重新查找。Locator 的选择器体系也比 Selenium 灵活得多。除了支持 CSS、XPath还内置了文本选择器、角色选择器、测试 ID 选择器。比如点击页面上唯一的提交按钮在 Selenium 里你可能要写一长串 XPath在 Playwright 里就是一句page.get_by_role(button, name提交)连按钮类型和可见文本一起约束了语义化程度高到像是写给人类看的。我再举个例子对比一下。Selenium 的写法from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver webdriver.Chrome() driver.get(https://example.com) try: button WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.XPATH, //button[contains(text(),提交)])) ) button.click() finally: driver.quit()Playwright 的写法from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch() page browser.new_page() page.goto(https://example.com) page.get_by_role(button, name提交).click() browser.close()后者没有显式的等待没有冗长的选择器没有手动的驱动管理。自动等待帮我们把什么时候能点这个问题解决了locator 帮我们把元素怎么描述这个问题解决了。这就是范式转移最直观的体现——从关心怎么做到关心做什么。3. 动手玩转 Playwright从安装到写第一段测试3.1 安装与环境准备理论说再多不如跑起来实战一次。我先讲 Python 生态下的安装方式因为这边用 Python 做测试的人最多。第一步是装库pip install playwright第二步是安装浏览器内核python -m playwright install chromium这里有一点要特别提醒如果你只是pip install playwright而没有执行第二步运行时往往会报错提示找不到浏览器可执行文件。很多新手在 Selenium 时代习惯了装个 pip 包再下载一个 chromedriver到了 Playwright 这里思路要换一下——浏览器内核不是去 Chrome 官网手动下的而是通过 Playwright 的命令统一管理的。如果你在 Windows 的 PowerShell 里执行playwright命令可能会看到一条报错playwright : 无法将“playwright”项识别为 cmdlet、函数、脚本文件或可运行程序。这种情况基本是 Python 的 Scripts 目录没有加入 PATH解决办法有两个一是执行的时候改用python -m playwright二是手动把C:\Python安装目录\Scripts加到系统 PATH 里。Node.js 生态同理遇到npx playwright找不到就检查一下 npm 的全局 bin 路径。浏览器下载慢是个高频问题。如果在公司网络环境下官方 CDN 经常连不上这时候可以设置环境变量PLAYWRIGHT_DOWNLOAD_HOST指向国内可访问的镜像地址这个做法和 pip 换镜像源的逻辑一模一样。要注意的是这个环境变量必须在执行install命令之前就配好不然进程一启动它就不知道去哪下载了。3.2 用 Codegen 录制一个真实场景Playwright 自带一个录制工具 Codegen这是在 Selenium 时代没有的体验。你在命令行里跑一句python -m playwright codegen它会弹出一个浏览器窗口同时打开一个代码生成面板。你在浏览器里点击、输入、跳转代码面板里会同步生成对应的 Playwright 脚本。这个工具用来快速搭建用例骨架特别好用尤其是面对一个你完全不熟悉的被测系统时录制一遍再人工整理比对着 DOM 元素写定位要快得多。我拿一个典型的购物车流程举例。假设要测试一个电商购物页面流程是打开商品详情页点击“加入购物车”进入购物车页断言购物车里出现了这个商品。用 Codegen 操作一遍生成的代码大概长这样from playwright.sync_api import sync_playwright def run(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://shop.example.com/products/123) page.get_by_role(button, name加入购物车).click() page.get_by_role(link, name购物车).click() page.get_by_text(无线蓝牙耳机).click() browser.close()这个脚本能跑但只是骨架。我一般会再补三件事加断言、加定位优化、加错误提示。比如把最后一步改成断言写法from playwright.sync_api import expect expect(page.get_by_text(无线蓝牙耳机)).to_be_visible()这样测试才真正有意义——它不只是走通流程而是在关键业务节点上做校验。Codegen 的价值是帮你省掉前期摸索 DOM 结构的时间但它生成的选择器不一定都适合长期维护比如有些自动生成的选择器会很长或者依赖了不稳定的 text 内容。录制脚本之后的人工清洗才是真正考验测试设计能力的地方。3.3 自动等待条件下的条件判断与断言Playwright 的断言库expect和传统断言不太一样它自带重试机制。你写expect(locator).to_be_visible()时它不会只判断一次而是默认在超时时间内反复检查。这跟 Selenium 里用assert element.is_displayed()是本质区别——后者是一次性判断页面稍微晚渲染 0.5 秒用例就挂了。我推荐的做法是在团队里约定一套断言规范凡是涉及页面渲染结果的一律用expect(...).to_be_visible()或to_have_text()不要用裸的assert。涉及数值判断的比如购物车数量可以先获取文本再转 int 判断也可以用to_have_value配合输入框场景。这套思路可以把用例的稳定性拉到很高的水位。还有一个 Selenium 时代实现起来很痛苦、Playwright 里很轻松的能力——网络拦截。比如测试一个下单失败的弹窗你在真实环境里很难构造出恰好失败的条件但可以用page.route把下单接口的返回内容直接伪造掉def mock_order_fail(route): route.fulfill( status500, content_typeapplication/json, body{error: 服务器内部错误} ) page.route(**/api/order, mock_order_fail) page.get_by_role(button, name提交订单).click() expect(page.get_by_text(下单失败请稍后重试)).to_be_visible()这相当于给你一个篡改网络请求的开关让异常场景的构造变得极其可控。这种能力在传统 Selenium 体系里基本没法干净实现除非自己起一个 mock server再手动把请求地址改过去麻烦得多。4. 常见问题与排查技巧实录4.1 安装与命令找不到的排查思路我把平时在社区和团队里看到的高频问题整理一下方便你遇到的时候快速定位。第一类是命令找不到。前面提到的playwright命令不可用大概率是 PATH 问题优先用python -m playwright绕开。Node 环境同理用npx playwright。这里有个细节如果你在终端里明明安装了 playwright但 npx 仍然找不到可以检查一下当前项目的node_modules/.bin是否在 PATH 里或者直接npm link playwright把命令暴露出来。第二类是浏览器启动失败。常见原因是系统缺少依赖库尤其在一些精简版的 Linux 环境上Chromium 启动时会报一堆libX、libGL缺失的错误。解决办法是在安装浏览器时带上依赖自动安装的参数比如python -m playwright install --with-deps chromium它会帮你把系统依赖一并装好。本机 macOS 相对省心Windows 一般也不会缺库但如果是在 CI 的容器里跑这条建议几乎必踩。第三类是端口冲突或资源占用。Playwright 启动的是一个自己的浏览器实例默认不跟系统里的 Chrome 共享所以一般不会有端口占用问题。但如果你用了pytest-playwright插件并且开了并行执行建议看一下 CPU 和内存是否够用尤其是跑 WebKit 内核时内存占用会比 Chromium 高一截。4.2 元素定位、iframe、多页面等经典痛点先说说 iframe 处理。Selenium 时代要切进切出Playwright 直接支持在 iframe 内部定位元素。比如页面里有一个嵌套的支付弹窗 iframe传统写法要switch_to.frame现在这样就行frame page.frame_locator(#pay-modal-iframe) frame.get_by_placeholder(请输入手机号).fill(13800000000) frame.get_by_role(button, name确认支付).click()不用切换上下文frame_locator返回一个定位器对象操作方法跟普通 locator 一样。嵌套 iframe 也可以一层层往下取代码依然很清爽。再聊聊多页面场景。点击一个target_blank的链接会打开一个新的标签页。Selenium 需要切 window handlePlaywright 可以用context.expect_page()来捕获新开的页面with page.context.expect_page() as new_page_info: page.get_by_text(查看订单详情).click() new_page new_page_info.value new_page.wait_for_load_state() expect(new_page).to_have_title(订单详情)这种上下文感知能力让我在写多标签页用例时几乎不用关心当前页是哪个框架自己知道。还有一个高频坑点击被遮挡元素。页面底部可能有吸底弹窗、cookie 提示条把目标按钮挡住了。Selenium 点击时经常抛 element not interactable你只能先手动关掉遮挡层。Playwright 里遇到这种情况优先确认是否为弹窗遮罩如果是则先关掉如果只是想快速通过可以调用locator.scroll_into_view_if_needed()把元素滚动到可视区域再用普通点击。但注意这可能是治标不治本真实用户可能也点不到那个按钮这是产品交互的问题测试该报就得报。4.3 调试工具Trace Viewer 与调试技巧以前用 Selenium 调试用例最痛苦的是错误只看得到一行抽象描述不知道实际操作到哪一步。Playwright 提供了一个非常实用的追踪工具 Trace Viewer可以在用例执行时记录每一步的 DOM 快照、截图、网络请求和控制台日志。启用方式是在 context 层面开启 tracingcontext browser.new_context() context.tracing.start(screenshotsTrue, snapshotsTrue, sourcesTrue) # 执行用例 context.tracing.stop(pathtrace.zip)然后命令行执行python -m playwright show-trace trace.zip它会打开一个可视化页面左侧是操作列表右侧是每个操作时刻的页面截图和 DOM 结构。用例挂了之后我一般不看堆栈直接看 trace 里最后一步操作是什么页面状态长什么样问题基本一眼就能定位。再推荐一个调试技巧失败时自动截图。配合pytest-playwright插件可以在 conftest.py 里加一段 fixture用例失败时自动截全屏并保存到 reports 目录。我习惯在截图命名里带上用例名和时间戳这样一堆失败报告摆在 CI 上时扫一眼文件名就知道是哪条用例、大概什么时间跑的。5. 范式转移对测试团队的意义5.1 测试代码的维护成本变化从一个团队协作的视角看Selenium 到 Playwright 的迁移表面上换的是 API实际上改的是编写测试的心智模型。Selenium 时代大家花大量时间在处理隐式等待、切换 iframe、管理窗口句柄这些事上测试用例读起来像是一堆底层操作的堆砌。而 Playwright 的用例更像一份场景描述打开什么页面、点击什么按钮、看到什么结果。维护成本也明显下降。自动等待和 locator 机制让用例变得更加稳定失败率能降一整个量级。我从多年实践里的观察是一个用了半年 Playwright 的团队用例维护工作量大约只有同等规模 Selenium 项目的三分之一。这不是说 Playwright 没有 bug、不会出怪问题而是它把大量容易出错的环境因素、时序因素接管了让测试工程师能把精力放在业务场景建模上。5.2 从录制脚本到 AI 辅助最近圈子里讨论很多的一个方向是把 AI 能力接进测试流程。Playwright 社区已经有人在尝试通过 MCP 协议把工具暴露给大模型让 AI 根据自然语言描述直接生成或修改测试用例。我个人的态度是工具可以在前期辅助生成骨架但测试设计、断言策略、数据准备这类涉及业务理解的部分短期内还是得靠人盯。AI 辅助最实际的落地场景是让 AI 读懂报错信息并给出定位建议或者根据录制操作生成初步用例脚本再由人来审查。这个流程比从零开始写要高效得多但也别指望完全自动化——测试用例是需要长期维护的资产设计不当的用例即使生成出来跑两天也得重写。收个尾一个老测试的心里话如果你问我现在新项目到底选 Selenium 还是 Playwright我的答案很明确新项目直接用 Playwright没有太多可犹豫的。它把 Selenium 时代那堆反人类的工作方式改得足够彻底自动等待、locator、网络拦截、trace 调试每一样都踩在我的痛点上。但我也想说一句不必急着把老项目一夜之间全部重写。Selenium 的存量用例如果跑得稳定先留着新页面、新模块用 Playwright 写等积累到一定量级再逐步把核心链路迁移过来。范式转移从来不是一蹴而就的工具会升级思路会迭代但测试的本质不变——用最可靠的方式验证真实用户在乎的那些事。我个人在实际操作中体会最深的一点是选工具只是一层真正决定用例质量的还是你对业务的理解和一套稳定的定位策略。工具给你提供了不需要手动等待的环境但设计出值得等待的用例永远是测试工程师自己的功课。
返回列表