
Playwright 这几年在 Web 自动化领域的上升趋势非常明显。不管你是 Selenium 的老用户还是刚准备入行写 UI 自动化应该都听过一个说法Playwright 正在取代 Selenium。我的判断是这个说法在“新项目选型”这个维度上基本成立。Playwright 把等待、定位、多页面、网络拦截、调试这些过去最让人头疼的事情都做进了框架底层。本文只写能落地的内容环境怎么准备、第一个脚本怎么写、从 Selenium 迁移要注意什么、批量跑测和 CI 怎么接以及最常见的报错怎么查。适合自动化测试工程师、开发自测任务以及准备给现有项目换框架的团队。1. 先确认一件事说 Selenium 退出到底是在说什么1.1 为什么 Playwright 能成为新的默认选项Selenium 不是不好而是设计太老了。它的核心思想还是 WebDriver 协议那一套脚本向浏览器发命令浏览器回响应。这套设计稳定但带来很多麻烦。比如你要先下载一个和浏览器版本严格匹配的驱动比如元素没出现的时候要自己写显式等待比如打开多个标签页要在window_handles里切来切去。Playwright 走的是另一条路线。它默认采用事件驱动和自动等待元素操作前框架会主动判断元素是否可见、是否可交互、是否被遮住。脚本里大量sleep和WebDriverWait可以删掉。而且 Playwright 自带浏览器版本管理不用手动匹配 driver这是它上手体验好的一个关键原因。有人一上来就问“到底该用 Selenium 还是 Playwright”。我的建议很直接如果是从零开始一个新项目团队也没有大量历史用例优先选 Playwright如果已经有几千条 Selenium 用例在跑则把迁移当成一个专项来评估而不是拍脑袋重写。有人还会混淆一个边界Playwright 主要解决 Web 端自动化。Android、iOS 的 App 自动化场景它并不直接负责那是 Appium 等移动端测试框架的领域。Web 页面里嵌套 WebView 是另一套复杂场景新手不要上来就把 Playwright 当成全端万能工具。1.2 Selenium 和 Playwright 的核心差异对比对比项SeleniumPlaywright等待机制常用 WebDriverWait 显式等待自动等待 expect轮询断言元素定位find_element/find_elementslocator体系语义化定位更丰富多标签页手动切window_handles每个页面天然是Page对象iframe需要切换上下文frame_locator直接定位网络拦截支持有限内置route可 mock、可改写请求和响应调试截图 日志截图 视频录制 Trace 回放驱动管理需要匹配浏览器版本playwright install自动管理录制工具Selenium IDE 偏重playwright codegen生成的脚本可用性高并行依赖外部执行器浏览器 context 隔离并行模型清晰这张表不是要全盘否定 Selenium。如果你的代码库大量依赖 WebDriver API或者还需要远程 WebDriver 分布式执行Selenium 依然能继续用。只是新项目里大家更少愿意再承担“驱动版本不匹配、等待写不完、调试靠打印”这些成本。这里比较关键的理解是Playwright 的并发模型和调试能力来自底层的 DevTools 协议和对浏览器生命周期的控制它不是一个换壳的 Selenium。所以迁移时不要只学 API要连测试思维一起换一个 context 就对应一套隔离的浏览器会话page 就是页面。把这层关系想清楚后面写多账号、多模块用例会轻松很多。2. 环境准备把安装和浏览器管理这两个老坑一次填平2.1 Python 环境下的安装命令很多人第一次用 Playwright会被安装步骤骗了。实际上核心只有两步pip install playwright playwright install chromium第一步是安装 Python 库第二步是下载 Playwright 维护的浏览器内核。注意第二步不是 pip 装完就自动完成的必须单独执行。如果你是在 Linux 服务器上跑自动化需要系统依赖库建议执行playwright install --with-deps chromium把系统依赖一起安装省得后面启动浏览器时报缺少共享库。也可以用playwright install不加浏览器名一次安装 Chromium、Firefox 和 WebKit。平时只做 Web 端业务测试先装 Chromium 就够了。注意playwright install必须执行单独安装 pip 库不会下载浏览器内核。这里顺便说一下 Selenium 老用户最容易卡住的问题浏览器驱动怎么选。Selenium 需要先查 Chrome 版本再去下载对应版本的 chromedriver版本对不上就启动失败。Playwright 把这步省掉了它自己管理浏览器内核和配套驱动不需要你做版本匹配。2.2 使用系统自带的 Chrome 而不是下载内核部分团队的测试环境要求必须用某个指定版本的 Chrome。这种情况可以不用下载 Playwright 内核from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse, channelchrome)设置channelchrome时Playwright 会寻找本机安装的 Chrome不再使用自动下载的 Chromium 内核。好处是测试环境和用户真实浏览器一致坏处是对本机浏览器版本有依赖团队内部需要约定统一版本。还有一个常见场景是内网环境下载不了浏览器。这时先确认是不是网络、磁盘或者依赖源配置问题再让运维帮忙把浏览器安装包同步到内网。不要一遇到下载失败就去搜各种“免安装方案”先把错误信息完整看一遍。我见过很多次所谓“Playwright 启动失败”最后都只是浏览器没有装好。2.3 用最短脚本验证环境安装完成后先跑一个最小脚本验证不要直接写完整用例from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://example.com) print(page.title()) browser.close()如果弹出浏览器并打印出页面标题说明环境和浏览器都正常。在 Windows、macOS 本地开发时headlessFalse方便观察在 Linux CI 上跑默认headlessTrue即无头模式。不要在本地和 CI 上使用完全相同的启动参数本地调试要看得见CI 要跑得快、不依赖桌面。3. 第一个 Playwright 脚本把 Selenium 的思路换过来3.1 一个登录后搜索的完整例子先看一个典型的用例打开登录页输入账号密码点击登录等待登录成功提示。from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://example.com/login) page.get_by_placeholder(用户名).fill(tester) page.get_by_placeholder(密码).fill(123456) page.get_by_role(button, name登录).click() page.get_by_text(登录成功).wait_for() print(login ok) browser.close()注意几个点get_by_placeholder是按输入框的 placeholder 定位get_by_role(button, name登录)是按按钮角色和名称定位。这两个方法都比拼 CSS 类名稳定页面调整布局时不容易挂。3.2 同样场景 Selenium 要怎么写如果换成 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/login) driver.find_element(By.CSS_SELECTOR, [placeholder用户名]).send_keys(tester) driver.find_element(By.CSS_SELECTOR, [placeholder密码]).send_keys(123456) driver.find_element(By.CSS_SELECTOR, button:has-text(登录)).click() WebDriverWait(driver, 10).until(EC.visibility_of_element_located((By.XPATH, //*[contains(text(),登录成功)])))两份代码核心流程几乎一样但脑子里要想的东西不一样。Selenium 要考虑显示等待写在哪、元素是否已经可点击、driver 版本对不对Playwright 默认会等到元素可操作再执行点击或输入。3.3 自动等待和断言别再到处 sleepPlaywright 的自动等待默认超时是 30 秒。实际操作时如果页面一直不满足条件会抛出超时异常而不是硬着头皮点一个不存在的按钮。这一点对 SPA 应用特别重要很多前端渲染是异步的元素出现、元素可点击、元素真正能响应点击是三个不同状态。Playwright 的 actionability 检查就是把这三步统一封装了。断言方面建议直接用expectfrom playwright.sync_api import expect expect(page.get_by_text(登录成功)).to_be_visible() expect(page.locator(#user-name)).to_have_text(tester)expect是带自动重试的断言。它会在默认超时时间内反复检查条件条件满足立刻继续条件不满足才报错。这比sleep固定等几秒要高效。注意如果脚本里到处是sleep说明还没有真正理解自动等待。迁移时最容易犯的错就是从 Selenium 带过来的坏习惯在 Playwright 里还继续加time.sleep(3)。如果发现脚本里到处是 sleep说明你对页面状态没有判断后面维护起来会很痛苦。4. 元素定位动态 id、复杂层级、iframe 都不要硬刚4.1 locator 是一套更完整的选择体系Playwright 里统一叫locator可以理解成一等选择器。除了常见的 CSS 和 XPath还有一组语义化方法get_by_text按文本定位get_by_role按无障碍角色定位比如 button、link、textboxget_by_placeholder按输入框提示文字定位get_by_label按表单标签定位get_by_title按 title 属性定位get_by_alt_text按图片 alt 文本定位实际项目里我最常用的是get_by_role和get_by_text。比如“编辑”按钮用 CSS 可能写成#app .row .btn-edit一旦前端改样式就失效但按钮的文字“编辑”基本不会变。用page.get_by_role(button, name编辑)语义稳定很多。4.2 链式定位和过滤器复杂列表页建议用链式定位先缩小范围再定位子元素row page.locator(#product-list tr).filter(has_text无线耳机) row.get_by_role(button, name上架).click()这就是先找到包含“无线耳机”的行再在这一行里找“上架”按钮。比一条超长 XPath 容易读也更容易维护。如果get_by_role或者locator匹配到多个元素会触发严格模式校验。这时用.first、.nth(2)或者继续加filter缩小范围。不要用.all()一把抓然后取第几个虽然能跑通但看起来不够清楚排查问题也麻烦。4.3 iframe 和动态内容很多业务系统里嵌了 iframe比如支付、地图、跨子系统的页面。Playwright 处理 iframe 比 Selenium 舒服得多不需要switch_to.frameframe page.frame_locator(#pay-iframe) frame.get_by_placeholder(银行卡号).fill(6222...) frame.get_by_role(button, name确认支付).click()frame_locator返回的是一个对象你可以像普通 page 一样继续定位。对于一个页面里有多个 iframe、iframe 内又有嵌套 iframe 的情况逐个frame_locator往下走就行。遇到动态 iframe核心问题往往不是 iframe 本身而是“它什么时候加载完”。你只需要把定位写在frame_locator上配合 Playwright 的自动等待通常框架会自己等到对应元素出现。先确认 iframe 的 id 或 name 是否稳定再去折腾内部选择器。5. 录制、多标签页和接口 mock三个最省事的进阶能力5.1 用 codegen 把手工操作转换成脚本Playwright 自带录制工具命令很简单playwright codegen https://example.com执行后会自动打开浏览器你在页面上操作代码区域会实时生成对应脚本。录制完成后可以切换生成语言支持 Python、Java、JavaScript、C# 等。我的使用习惯是把它当成“定位器生成器”先在页面上点击、输入、跳转把主流程录下来。再用浏览器 DevTools 查看实际 DOM确认录出来的 locator 能不能更稳定一些。最后手动补断言、清理脏数据和前后置条件。codegen 生成的脚本可以用但不能直接当最终用例。因为它默认只覆盖你手工操作的那条路径不会自动处理账号隔离、数据清理、异常分支和并发问题。5.2 多标签页和浏览器上下文隔离这是 Playwright 和 Selenium 差异很大的地方。Selenium 用window_handles来回切换Playwright 直接给每个标签页一个独立对象。context browser.new_context() page1 context.new_page() page2 context.new_page() page1.goto(https://example.com/a) page2.goto(https://example.com/b)context是浏览器上下文类似一套独立的用户会话。不同 context 之间的 cookie、localStorage、缓存相互隔离。写多账号用例时每个账号开一个 context就不会出现串登录的问题。如果要在新标签页打开某个链接可以先设置跳转监听with context.expect_page() as new_page_info: page.get_by_role(link, name打开详情).click() new_page new_page_info.value new_page.wait_for_load_state()这里关键不是代码多酷而是理解生命周期浏览器、上下文、页面三层。很多Target closed报错都是这三层被提前关闭或者关错了对象。5.3 网络请求拦截和 Mock业务测试经常依赖第三方接口如果第三方不稳定用例也跟着不稳定。Playwright 的route可以拦截请求返回自定义结果。def mock_handler(route): route.fulfill( status200, content_typeapplication/json, body{code:0,data:{name:mocked}} ) page.route(**/api/user/getInfo, mock_handler)也可以不 mock而是放行真实请求并改写响应def modify_handler(route): response route.fetch() body response.body().replace(b正常, b异常) route.fulfill(responseresponse, bodybody) page.route(**/api/order/list, modify_handler)还有更常见的提速玩法直接把图片、字体、视频等静态资源拦截掉page.route(**/*.{png,jpg,jpeg,gif,woff2,mp4}, lambda route: route.abort())这对大型后台系统很有用测试核心业务逻辑时加载一堆图片字体纯属浪费带宽和等待时间。注意如果用例要验证图片是否正常显示就不要做这个拦截。6. 批量跑测、并发和 CI从能跑到稳定跑之间还有几条坎6.1 用 pytest-playwright 组织用例写单个脚本只能叫 Demo到了团队协作阶段得把用例组织成测试框架。Playwright 官方提供了pytest-playwright安装后直接在测试函数里使用pagefixturepip install pytest-playwrightdef test_login_success(page): page.goto(https://example.com/login) page.get_by_placeholder(用户名).fill(tester) page.get_by_placeholder(密码).fill(123456) page.get_by_role(button, name登录).click() expect(page.get_by_text(登录成功)).to_be_visible()pagefixture 已经帮我们接好了浏览器生命周期和失败截图不用每个用例都写with sync_playwright()。6.2 并发策略先看资源再看 worker 数很多人一上来就想并行加速。我的建议是先把单条用例跑稳再开并发。并行执行通常用pytest-xdistpip install pytest-xdist pytest -n 4-n 4表示启动 4 个并行 worker每个 worker 一个独立 python 进程也意味着会有多个浏览器实例同时跑。这里要特别注意资源占用。Playwright 每个浏览器进程大概会占用几百 MB 到 1GB 内存还要算上页面渲染的额外消耗。如果测试机只有 8G 内存不要开 16 个并发。先从 2 到 4 个 worker 开始观察内存和 CPU再逐步增加。并发还会带来数据隔离问题。多个 worker 同时操作同一个账号、同一个订单系统会造成数据冲突。最好每个并发用例使用独立账号或者测试数据本身允许重复创建。写用例时不要偷懒复用同一个账号。注意并发 worker 数不是越高越快先看测试机内存和 CPU。6.3 headless 模式和 CI 集成的常见姿势本地跑 UI 测试可以开浏览器窗口CI 里通常用无头模式这样不依赖桌面环境。pytest --headed pytest --tracingon--tracingon会记录用例执行的轨迹文件失败时可以用 Playwright 的命令打开 Trace Viewer 回放比只看截图更容易定位问题。CI 上最容易遇到的是 Linux 系统库缺失。本地 Windows 能跑一到 Linux 容器就报浏览器启动失败多数是系统依赖库没装。建议在 CI 基础镜像执行playwright install --with-deps chromium如果是 Docker 环境还要确认浏览器沙箱相关配置。具体怎么启动要以你们团队的安全规范和官方容器镜像说明为准不要为了图省事在不可信环境里随意关闭安全隔离。7. 高频报错不要慌按这套顺序查一般十分钟能定位7.1 几个高频报错的直接原因报错信息常见原因怎么处理Target closedcontext 或 page 被提前关闭还在继续调用检查对象作用域不要在with结束后再用 pageTimeout 30000ms exceeded元素没出现、不可操作或 locator 写错先用page.locator(...).count()确认元素存在再检查定位是否匹配Strict mode violationlocator 匹配到多个元素用filter、.nth()、.first收窄范围Executable doesnt exist浏览器内核没下载执行playwright install chromium启动时缺系统库Linux 环境依赖不完整执行playwright install --with-deps沙箱相关报错Docker 或特定权限环境下运行检查 CI 容器配置和安全规范7.2 定位器超时不是马上改代码遇到超时第一反应不应该是把默认超时从 30 秒改成 60 秒而是先问几个问题这个元素是不是真的存在直接在浏览器里手动打开对应页面用 DevTools 搜索文本或 selector。它是不是在 iframe 里在页面里看 DOM 树元素全部包裹在某个 iframe 里那就在frame_locator下定位。它是不是有多个匹配很多按钮在不同弹窗里名字一样用严格模式确认。它是不是异步渲染如果是Playwright 的自动等待会解决不用手工 sleep。如果这些都没问题再把定位器写得更精确。比如一个“确定”按钮可能在弹窗、抽屉、页面本身都有那就先从页面里找弹窗的父容器再在容器内定位。7.3 一套可以复用的排查链路我会按这个顺序处理问题看现象是启动失败、定位超时、断言失败还是页面崩溃看浏览器本机手动打开这个页面走到同样操作页面是否正常看环境本地能跑、CI 不能跑先对比系统依赖和浏览器版本。看日志Playwright 报错信息里通常会带上 locator 表达式和等待过程仔细读最后几行。看修改记录昨天能跑、今天挂了先看页面是否改版再回滚测试代码验证。这套顺序比一上来就搜报错更高效。很多所谓“框架不稳定”最后都是页面改了、数据被污染、或者 locator 匹配到了多个元素。8. Selenium 还有哪些场景要留以及怎么把切换做成增量工程8.1 不一定立刻迁移的几种情况虽然 Playwright 在大多数新项目里更有优势但下面几种情况不建议硬迁存量 Selenium 用例量很大团队短期内没有精力重写。测试基建已经深度绑定 Selenium Grid、远程 WebDriver 或某类商业测试平台。有旧版浏览器或 IE 兼容需求Playwright 并不支持老旧 IE 生态。团队主要维护的是几十条简单回归用例重写收益不高。技术选型最忌讳“因为大家都说好就重写”。更好的做法是选一个稳定的业务模块用 Playwright 做试点跑一段时间看稳定性、维护成本和执行效率用数据说话。8.2 切换建议增量试点而不是推倒重来我建议分四步切换选一条用户主路径比如“登录 - 列表 - 详情 - 下单”用 Playwright 重写。和现有 Selenium 用例并行跑一周对比失败率和耗时。稳定后让团队新用例默认用 Playwright 写。老用例逐步迁移迁移优先级按业务重要程度和失败率决定。切框架最大的成本不是 API 差异而是之前积累的测试设计思路。Selenium 时代常用“先等待、再操作、再验证”的思路没有变只是 Playwright 把等待内化到了操作里。8.3 学习路线和最后一个建议如果现在准备系统地学习 Playwright我的建议顺序是先把 locator 体系和自动等待彻底搞明白这是地基。写通一条完整业务链路的用例包含页面跳转、表单输入、按钮操作、断言。学会用 codegen 和 Trace Viewer这两个工具能节省大量定位和调试时间。再学网络拦截、多 context 隔离和 pytest 集成。最后再碰并发、CI 和 Docker。看官方文档时如果英文吃力可以找社区维护的 Playwright 中文手册辅助阅读但接口签名和参数以官方英文文档为准。另外如果你在用 AI 编程工具现在也有 AI 编码助手支持 Playwright 的 MCP 集成可以让模型辅助操作浏览器、复现前端问题。这个方向还在快速变化建议先把 locator、context、page 这些基础概念掌握再去看 MCP 相关配置。很多人学了一半突然跑去研究怎么让自动化脚本不被网站识别。这个话题我不建议花精力尤其是对别人的网站。做正常测试你只需要确保脚本稳定、数据隔离、用例可维护这些能力比任何“绕过检测”的技巧都值钱。最后留一句实操经验新项目切 Playwright 后稳定执行的关键不是框架选择而是测试数据隔离和失败可观测。把 context 会话、账号数据和 trace 记录提前设计好比纠结一两个 API 方法重要得多。