
干UI自动化这些年Selenium陪我走过了最艰难的一段路也让我吃够了它的苦。过去写脚本一半时间在写逻辑另一半时间在处理各种元素不出现、窗口切换丢状态、iframe死活定位不到的问题本地跑得好好的一上CI就随机失败。后来真正把Playwright引入核心项目我才意识到这不只是换一个工具那么简单而是整个UI测试思路的换血——从“你配合浏览器”到“浏览器配合你”。这篇不打算念文档我想站在一个踩过无数坑的测试开发角度聊聊从Selenium到Playwright这一年多我看到了什么、踩了什么坑、怎么把新框架真正落到项目里。对这种转型我理解的结论是它能解决的不是单个用例不稳定的问题而是整个UI测试体系的可维护性问题。无论你是刚入行还在纠结学哪个框架的新手还是被Selenium折磨了多年的老手这篇都应该有些你能直接拿走的东西。1. 范式转移的本质Selenium在等Playwright在保证“范式转移”这个词看着唬人但放在UI测试工具这个圈子里它其实特别具体。Selenium和Playwright看起来都是“打开浏览器、点按钮、填表单、看结果”很多使用层的东西也很像但两者的底层设计哲学完全不同。这个差异决定了你写出来的测试是脆弱的还是稳的也决定了维护一套自动化测试究竟是在给自己减负还是在给团队添堵。1.1 协议之争一个像遥控器一个像坐在驾驶舱Selenium走的是WebDriver协议本质上是浏览器外部的一套标准化JSON命令集合。你在测试脚本里发一条“点击元素”的指令WebDriver的中间层找到对应浏览器的驱动驱动再去调用浏览器原生接口执行操作。这套设计在上古时期解决了“能不能用一个API操作所有浏览器”的问题它的贡献是不可替代的但它的局限在于外部指令只能做到“发出去就完了”对浏览器内部的实时状态基本无感。Playwright走的是另一条路。它最初基于Chrome DevTools Protocol直接和浏览器底层通信后面也实现了对其他浏览器的类似协议支持。这就意味着它能订阅浏览器内部事件、看到网络请求和响应、操作缓存、拦截资源加载、监听控制台日志。打个比方Selenium像一个拿着遥控器站在电视外面的观众只能按台Playwright像一个坐在驾驶舱里的人能看到仪表盘也能直接拨弄各种开关。这个底层差异直接决定了后面一系列能力差异接口mock、追踪回放、性能数据收集、多页面事件等待这些在Selenium体系里很难做或者要引入一堆额外工具在Playwright里是原生能力。这个协议的差异还带来了一个实际影响多浏览器支持的范围变了。Selenium覆盖的浏览器种类多但各家驱动版本和浏览器版本经常互相打架Playwright支持Chromium、Firefox、WebKit覆盖面够用而且每个核心版本会同步更新浏览器内核你不用自己折腾驱动匹配。对多数项目来说这三个内核已经能覆盖用户真实使用的绝大多数场景。1.2 自动等待从“写好等待”到“不用写等待”Selenium项目里最常见的一段代码长这样先find_element再判断是否可见再sleep几秒再点击。资深的会写显式等待封装ExpectedConditions但本质上还是在“猜”和“试”。问题在于UI元素的状态变化不如人预想接口慢半秒、动画多一秒、数据渲染延迟都会让sleep短了或长了。sleep是万恶之源但过去没人能绕开它。Playwright把等待内置进每一个操作里了。它的所有API在真正执行前会做一组可操作性检查包括元素是否已经附加到DOM、是否可见、是否稳定坐标不再变化、是否接收事件、是否已启用。框架会按照你设置的超时时间反复等待检查通过默认30秒直到元素可操作才真正执行点击。换句话说你写page.locator(button.login).click()Playwright帮你做的不仅仅是一次点击而是“等到这个按钮可点再点”。这彻底解决了大量因为加载慢而导致的偶发失败。我第一次用的时候确实有点不适应因为思维还在“要先sleep一下”的惯性里。后来我把项目里所有手工sleep删干净稳定性反而提升了执行时间也缩短了。这不是魔法是框架把过去你手动做的事自动化了而且做得比你规范。2. Selenium的黄金时代和三个让我头疼的夜晚在吐槽Selenium之前我先把它的历史位置摆正。没有Selenium就没有今天UI自动化的繁荣。它是二十年来Web测试最普及的框架社区积累的文档和案例多到看不完它帮我解决了当年最急迫的问题一套脚本能在不同浏览器上跑起来。任何后来者和它比较时都应该先承认这一点。但是真正的项目经验会告诉你Selenium的很多问题不是使用姿势不对而是架构层面的天花板。下面这三个痛点我想每个Selenium老用户都熟悉。2.1 痛点一等待靠脑补稳定全凭运气Selenium的隐式等待和显式等待是经典大坑。隐式等待只针对findElement阶段如果元素已经存在但被遮挡或还没可操作点击照样失败。显式等待虽然能用但要写好它需要很强的经验每个案例都要按不同状态去写比如visibility_of_element_located、element_to_be_clickable、presence_of_element_located光记住这些条件就够喝一壶了。更麻烦的是隐式等待和显式等待混用会导致WebDriver的轮询逻辑变得混乱实测中会出现等待被无效化的情况。所以很多人最终为了省事直接用time.sleep。代码看起来简单了执行效率却被无限拉低而且不稳定。你sleep两秒本地还行到了CI上机器负载一高两秒不够随机挂。这种大量随机失败会严重消耗团队对自动化测试的信任最后变成“跑了也白跑看到红灯就重新点一遍”。2.2 痛点二iframe和多窗口像迷宫Selenium里处理iframe需要手动switch_to.frame处理多窗口需要switch_to.window然后还要管理句柄集合。一次两次还好一旦页面里有动态iframe或者点击一个按钮后新窗口打开得慢你的代码就得写一堆轮询等待加上句柄比对。我现在还记得那个场景某个项目里嵌了第三方支付流程页面一层iframe套一层iframe动态创建、动态销毁。Selenium脚本要在alert、默认内容、不同iframe之间反复横跳稍微跳错一个层级就报NoSuchElementException。光维护这套切换逻辑就花了我两周时间。Playwright的处理方式完全不同它提供frameLocator定位iframe里的元素不需要离开主文档上下文直接像在普通页面里一样构造定位链。多页面用context.waitForEvent(page)就能拿到新打开的页面对象完全不用数句柄。这种设计上的简化不是语法糖而是从根本上消除了一个复杂状态管理的负担。2.3 痛点三调试基本靠猜Selenium脚本失败后你能拿到的信息就是异常类型和一句错误说明定位失败的原因基本靠推测。你想知道元素为什么找不到只能看HTML源码你想知道点击到底发没发出去只能再查后端日志。截图功能有但默认失败的截图往往只截到“当时的画面”看不到“之前发生了什么”。等你想排查网络请求、控制台报错Selenium原生能力几乎为零要额外接Selenium Grid、接监控平台、做日志收集。所以真实工程里Selenium用例出问题后的三连问通常是是不是没等到是不是元素变了是不是浏览器版本不对要花大量时间去猜去验证。而Playwright的Trace Viewer让我第一次体验到了“回放式排查”它能记录整个测试过程的每个步骤、每个操作的DOM快照、网络请求和页面截图失败后打开Trace一看问题在哪一目了然。3. Playwright真正赢在哪些细节如果说自动等待是第一眼能看到的核心优势那Playwright真正让我产生“再也回不去了”的感觉的是它在工程细节上的碾压式设计。这些东西看起来每个都是小功能但组合在一起省掉的是整个测试基建的时间。3.1 Locator API告别脆弱的选择器Selenium里写元素定位最常见的是XPath和CSS选择器。XPath功能强大但写长了一串//div[classa][data-idb]页面上稍微改个class就崩。Playwright官方推荐的方式是语义化定位getByRole、getByText、getByLabel、getByPlaceholder、getByTestId。这些API模拟的是“用户眼里怎么找这个元素”而不是“开发者结构上写了什么”。更关键的是Playwright默认启用严格模式。当你的定位方式匹配到多个元素时它不会像Selenium那样默默选择第一个匹配的而是直接报错并告诉你定位到了几个、选择了哪些。这个“严格模式”在开发前期让人难受因为报错频繁但它逼着你把选择器写精确反而从源头上避免了“选错元素还测不出来”的线下事故。我经历过一次Selenium脚本测的是商品列表里的第一个商品但页面加了推荐位后脚本点成了推荐位里的商品用例全挂。这种问题在Playwright的严格模式下写脚本的那一天就会被发现。3.2 网络拦截与接口mock测试不再依赖后端我特别想聊一下page.route这是Playwright里我使用频率最高的功能之一。它允许你拦截浏览器的所有网络请求可以修改请求、mock响应、直接abort掉某些请求甚至模拟延迟和失败情况。对UI测试来说这意味着什么呢举个实际例子。我们测试一个购物车页面最怕的是支付回调不稳定频繁出现等待超时或者环境数据被污染。用Playwright后可以拦截支付回调接口不真实走支付直接把测试需要的返回体塞回去购物车逻辑照常往下走。前端测试再也不依赖后端环境了CI上跑自动化测试时不会被某个不稳定的服务拖垮。我在Selenium时代靠的是改hosts、搭一套完整测试环境、写一堆mock服务在Playwright这里三行代码就搞定。这个能力还能用来验证前端对接口异常的处理逻辑把接口mock成返回500看页面有没有正确的错误提示这在Selenium时代基本没法做。3.3 Trace Viewer与录制工具调试体验和上手门槛一起被解决Playwright的录制工具Codegen是我给团队新人推荐的第一站。运行npx playwright codegen your-site.com或者python -m playwright codegen your-site.com它会打开一个浏览器窗口你在页面上每操作一步旁边就自动生成对应的脚本代码。生成的代码质量非常高直接就是推荐写法定位器语义化、自动等待天然存在。团队里刚接触自动化测试的同学靠这个工具半小时就能写出第一个像样的用例这在Selenium时代想都不敢想。但要注意录制工具是草稿纸不是成品。它生成的代码经常会有冗余的中间步骤比如某些不必要的滚动、模板化的expect。真正提交到仓库之前还是要手工整理合并可以合并的步骤、删掉多余的点击、把选择器改成稳定的语义化写法。我见过很多新人直接使用录制好的脚本结果一个用例几百行后期维护到崩溃。正确姿势是录一个骨架然后基于这个骨架手工打磨。3.4 从录制到AI助手MCP给测试带来了一种新工作方式Playwright的生态也一直在往前走。最近社区和官方都在推把Playwright接到AI Agent里比如Playwright MCP Server和Codex插件让大模型可以调用Playwright的能力去操作浏览器、读取页面结构、生成测试脚本。实际用下来它已经能完成很多探索性测试的初稿编写比如给一个地址AI能自己打开页面、点击各个入口、发现明显异常并生成一个测试用例草稿。我觉得这里有个重要的态度问题AI不会完全替代测试工程师它更像一个不需要休息的实习生加了一个靠谱的浏览器操作API。但对团队来说它显著降低了从“页面原型”到“测试脚本初稿”的转化成本让测试人员能把更多精力放在复杂场景和断言设计上。我现在的习惯是让AI先跑一遍页面生成脚本草稿我再人工审查和补场景。人机配合的效率比纯人工写脚本快了一倍不止。4. 从零落地Playwright安装、录制与第一个自动化测试理论讲再多不如动手写一个。这里我用Python版本为例因为做测试的同学普遍更熟悉Python。整个流程从安装到跑通第一个用例大概十几分钟前提是你有一个能正常访问的目标网页。4.1 环境准备与安装避坑先说明一个常见误区安装Playwright分两步第一步是安装Python库或Node库第二步是下载浏览器内核。很多人只做了第一步运行时报错找不到浏览器。pip install playwright playwright install chromium第一条命令装的是测试框架本身第二条命令下载Chromium浏览器内核到本地缓存目录。这一步在国内网络环境里经常遇到下载超时解决方案是配置镜像环境变量比如用阿里的镜像源set PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright/ playwright install chromium如果你在Windows上遇到“playwright : 无法将‘playwright’项识别为 cmdlet、函数、脚本文件或可运行程序”的报错说明CLI命令没进PATH。这个大概率是只装了Python库但环境变量里没有Python Scripts目录或者用Node版本时没有通过npx调用。最简单的排查方式先用python -m playwright --version确认库是否装好能跑就说明库没问题再去调PATH。装完浏览器后我建议顺手跑一遍系统依赖检查。在Linux环境需要装一些系统库不然浏览器起不来。Mac和Windows一般没这个麻烦。4.2 用Codegen快速生成第一个脚本环境就绪后直接启动录制工具python -m playwright codegen https://example.com浏览器会弹出来旁边是脚本窗口。你随便点点页面的链接、填个表单脚本窗口里的代码会实时更新。做完操作后把生成代码粘到一个test_xxx.py文件里最简结构长这样from playwright.sync_api import sync_playwright def test_first_case(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://example.com) page.get_by_role(link, nameMore information).click() print(page.title()) browser.close()运行用pytest直接执行就行前提是装了pytest。Playwright本身也提供pytest插件可以把browser、page这类对象当作fixture直接注入但刚上手时用最朴素的写法反而更容易理解。4.3 一个完整案例购物车测试从点击到断言实际项目里核心业务流程的测试比demo复杂得多。我拿一个典型购物车场景演示打开商城首页、搜索商品、打开详情页、点击加入购物车、断言购物车数量变成1。这个场景覆盖了跳转、动态数据加载、组件状态变化很适合拿来体验自动等待和语义化定位。from playwright.sync_api import sync_playwright def test_add_to_cart(): with sync_playwright() as p: browser p.chromium.launch(headlessTrue) page browser.new_page() page.goto(https://your-shop-site.com) # 搜索商品get_by_placeholder 直接定位输入框 page.get_by_placeholder(搜索商品).fill(机械键盘) page.get_by_role(button, name搜索).click() # 等待搜索结果中出现第一个商品标题 product_title page.get_by_role(heading, name机械键盘 K870).first product_title.click() # 详情页加入购物车 page.get_by_role(button, name加入购物车).click() # 断言购物车数量变为 1 cart_badge page.locator(.cart-badge) cart_badge.wait_for(statevisible) assert cart_badge.inner_text() 1 browser.close()注意代码里的get_by_role这是按ARIA角色加可访问名称来定位的页面结构改css类名基本不影响它。搜索框用get_by_placeholder针对输入框尤其好用。关于购物车徽标用了wait_for显式等待这是因为加入购物车通常有异步请求和角标动画等它出现再断言才稳。实际业务中这一步也可以直接依赖断言方法自带的重试机制我习惯对关键状态写一下意图更清晰。这个用例里没有一处time.sleep却比当初Selenium版稳定得多执行时间也快。核心原因是等待逻辑被分布式地嵌入到每个操作里而不是靠人工统一估算。4.4 运行用例与测试报告跑用例很简单pytest test_add_to_cart.py --headed加--headed是让你能看到浏览器过程调试时用。调试完去掉CI上无头模式跑速度更快。如果想看更多运行细节可以加--tracingon失败时自动生成trace文件再用playwright show-trace命令打开回放。团队协作时我建议把测试报告也接入pytest-html或者Allure把失败用例的截图和trace文件作为附件挂上去开发同学拿到一条链接就能定位问题不用再跑到你电脑上看log。5. 常见问题与排查技巧实录从Selenium迁移过来的团队在新框架里遇到的第一批问题基本是同一批。我整理了一张速查表按遇到频率排的问题现象根因解决办法找不到元素报TimeoutError页面加载慢或元素在iframe里检查元素是否在iframe中用frameLocator定位调大timeout配置提示strict mode violation定位方式匹配到多个元素改用get_by_role加名称、get_by_text加精确/模糊选项或者用locator.filter打开浏览器黑屏/崩溃Linux系统缺依赖库运行playwright install-deps网络请求被拦截后页面异常route拦截逻辑写得太宽缩小匹配范围用正则精确匹配接口路径新浏览器版本导致脚本失败固定内核版本变了升级Playwright版本或锁定内核版本到pins录制工具生成代码又长又乱录制步骤包含大量冗余点击录制后手动整理删除无用步骤优化选择器测试跑得慢未做并发或太多等待关闭无用的trace和视频用worker多线程跑几个深入说明第一iframe的问题最多。很多测试服务器里页面结构是“主页面套iframe套iframe”Playwright虽然改进了体验但如果你还是用page.locator去定位iframe里面的元素一样找不到。正确做法是page.frame_locator(#main_iframe).get_by_text(提交)。并且要注意iframe的加载时机用frame_locator内部操作会自动等待但如果你要等待iframe本身出现可以用page.wait_for_selector(iframe[namexxx], stateattached)。第二严格模式报错别慌。它报错反而是好事说明你定位写得不够精确。曾经有一次我的get_by_role(button)匹配到两个按钮加上name过滤就行。如果没有明显的name可以用locator的filter根据文本内容缩小范围或者用and连接两个条件。第三关于动态加载的iframe和shadow DOM。现在很多前端框架喜欢用第三方嵌入组件页面里动态创建iframe内容和结构都不可预判。团队里如果碰到这类情况优先建议开发配合加data-testid属性测试用page.get_by_test_id定位稳定性和可维护性都最好。实在没法加测试属性时才建议用相对复杂的CSS选择器配合filter并且要把这些复杂定位统一封装到Page Object里避免散落各处。6. 要不要迁移老项目怎么办很多团队看完上面的优势热血上头想把手头Selenium项目全部重写。我的建议是别急着推翻重来。6.1 先评估你的自动化测试累不累如果现在的Selenium项目跑得挺稳、维护成本可控、团队也没有明显的痛点那不一定非换不可。自动化测试是业务工具不是秀技术的新玩具。技术债和迁移成本都是真实存在的尤其是存量用例量大、页面对象封装成熟的项目全量重写可能耗费两三个月中间还可能出现回归覆盖断档的风险。但如果你的项目出现下面这些信号那确实该考虑切换了用例失败率常年在10%以上、修脚本的时间比写新用例的时间还长、每个开发同学都有改测试的恐惧、接口变动导致大量用例连带崩溃、新功能测试迟迟加不进去。这些都是测试体系在拖后腿的信号换框架是治本的方案。6.2 渐进式迁移的三条经验我自己的团队做完切换用的不是“推倒重来”而是“新老并存、渐进替换”。第一步新需求、新模块的UI测试一律用Playwright写不写新的Selenium代码。第二步挑现有用例里失败率最高的五条用Playwright重写跑一段时间对比两者稳定性数据。这一步很关键用数据向团队证明“换框架能降低抱怨成本”。第三步慢慢把高频核心用例迁移过来边缘用例继续留在老框架里直到Selenium用例数量自然降到零。三条经验供参考一是迁移时不要机械翻译老用例。老用例很多是建立在大量补丁之上的比如各种sleep、强行try-except、复杂的显式等待。重写时应该回到业务场景本身重新审视这个用例到底要验证什么然后按Playwright的思路重写而不是把Selenium的坏习惯带过来。我见过有人把Playwright用例写成sleep遍地的版本等于白迁移。二是尽早沉淀Page Object。Playwright的locator API虽然好用但如果每个测试文件里都写一堆page.get_by_role(button, name加入购物车)页面结构一变就得全局搜索替换。建议把公共页面操作封装成Page Object类比如ShopPage.add_to_cart()这样即使未来页面重构只需改一个文件。三是把headless和headed分开看。CI里跑headless确实稳定但真正常用应用特别是图表类WebGL页面无头模式可能有隐藏差异。建议定期在本地有头模式跑一遍关键用例两边都过才放心。最后分享一点个人体会切换Playwright之后最大的收益不是脚本变短了也不是失败率降了而是我终于敢让自动化测试推进到发布流程里了。Selenium时代UI测试失败总让我怀疑是脚本的问题Playwright时代脚本报错我敢大胆说是应用的问题因为框架把等待、状态、追踪这些事情做扎实了脚本本身的可信度上来了。如果有人正在Selenium的泥潭里挣扎我的建议很简单找一条平时最容易红的用例用Playwright重写一遍跑上几天亲自感受一下自动等待和trace带来的变化。好的工具不是魔法但好的设计真的能让你少熬夜。