1. 为什么我从 Selenium 转向了 Playwright1.1 Selenium 陪伴我的那些年以及它逐渐暴露的问题做 Web 自动化测试的人几乎没有人能绕过 Selenium。我最早接触 Selenium 是在 2016 年左右那时候项目里用的是 Selenium 2 WebDriver Java 的组合配合 TestNG 管理用例、生成报告。说实话在那个时代它几乎是唯一的选择功能齐全、社区庞大、资料丰富遇到问题随便一搜就能找到答案。哪怕到今天很多公司招聘 JD 里写的依然是熟悉 Selenium可见它在业界的根基有多深。但用了七八年之后越来越多的痛点开始浮现。先说说最明显的Selenium 的等待机制依赖 sleep 和显式等待。页面加载慢一点、接口响应快一点脚本就容易不稳定。我经历过无数次因为sleep(3)不够或者WebDriverWait超时导致的 CI 误报半夜爬起来看日志最后发现只是某个静态资源加载慢了几百毫秒。这个问题并不是不能解决但它让我们团队不得不花大量时间维护等待逻辑而不是把精力放在业务断言本身上。再一个是Selenium 对现代 Web 应用的支持不够原生。SPA 应用里大量的 iframe、shadow DOM、Canvas 渲染、文件下载、多标签页切换用 Selenium 做起来非常别扭。比如处理 shadow DOM需要先执行一大堆 JavaScript 才能拿到内部元素然后封装成工具方法每个同事的使用方式还不一样代码风格千奇百怪。第三个困扰是安装配置的复杂度。Selenium 本身只是一个客户端库浏览器驱动需要单独下载Chrome 升级了驱动版本忘了同步又是 environment 层面的问题。Selenium Manager 是后面才出的早期版本完全没有每个团队成员都要手动装 driver非常痛苦。我不否认 Selenium 的历史地位它在 Web 自动化领域做出了巨大的贡献。我只是觉得当 Playwright 这样的工具出现以后我们再继续守着 Selenium 老一套其实是拿自己的效率和时间在买单。1.2 Playwright 真正解决了什么为什么值得迁移第一次打开 Playwright 官方文档的时候我印象最深的一句话是Playwright 通过 BrowserContext 提供隔离的浏览器会话每个上下文之间互不干扰。这句话背后的含义远不止字面意思。它意味着你可以像操作真实的浏览器环境一样在同一个浏览器实例中创建多个独立的会话各自持有独立的 cookie、storage 和缓存状态。这对多用户场景的测试来说极其有用——比如同时模拟管理员和普通用户在同一系统里的不同操作路径用 Playwright 只需要创建两个 context而用 Selenium 需要反复切换 cookie 或者起多个 driver 实例。更让我眼前一亮的是它的自动等待机制。Playwright 的大多数操作点击、输入、选择都会自动等待元素可操作。这个可操作不是简单地判断元素存在而是会检查元素是否可见、是否处于稳定状态、是否被其他元素遮挡、是否已经响应事件。这意味着我几乎不需要在代码里写time.sleep()或者ExplicitWait。它从根上解决了 Selenium 最让人头疼的 flaky 问题测试用例稳定率明显上了一个台阶。还有内置的多浏览器支持。Playwright 官方同时支持 Chromium、Firefox 和 WebKit 三种浏览器内核而且同一套 API 在不同浏览器上的行为高度一致。我只需要改一行配置就能让整个测试套件在三种浏览器上跑一遍跨浏览器兼容性问题在发版前就能暴露出来。我最喜欢的一点是它对现代 Web 特性的原生支持。iframe、shadow DOM、文件上传下载、多标签页、桌面通知、权限申请这些都是开箱即用的能力不需要自己封装该死的手写 JS 注入。代码写起来干净、直白、维护成本低。刚才提到体验问题时还有一个隐藏加分项Playwright 的调试体验极其优秀。npx playwright codegen可以录制操作并生成代码playwright trace viewer可以回放每一步操作并查看网络请求、DOM 快照、控制台日志排查问题的时候比 Selenium 时代全屏打日志高效太多了。所以我的结论很明确Playwright 不是 Selenium 的简单替代品而是一次范式升级。凡是还能用 Selenium 硬扛的项目换了 Playwright 之后基本都会有一种原来自动化可以这么舒服的感觉。2. 环境准备与第一个自动化脚本2.1 安装与配置从零开始跑通 Playwright不管你是用 Python 还是 Node.js安装 Playwright 的过程都非常简单。以 Python 环境为例只需要三步pip install playwright playwright install playwright install-deps第一条命令是安装 Python 客户端库第二条命令会下载 Chromium、Firefox 和 WebKit 的浏览器二进制文件第三条命令是安装系统依赖这一步在 Linux 服务器上尤其重要不装的话启动浏览器时会报一堆缺 .so 库的错误。如果是在 Windows 的本地开发机上跑install-deps不是必须的但官方仍然建议执行可以避免一些莫名其妙的字体或媒体解码问题。Node.js 环境对应的是npm install playwright npx playwright install这里有个细节需要注意浏览器二进制文件默认下载到用户目录下的缓存文件夹中Windows 下在%USERPROFILE%\AppData\Local\ms-playwrightLinux 下在~/.cache/ms-playwright。如果你的 CI 环境是 Docker建议在构建镜像时先执行playwright install把浏览器缓存到镜像里否则每次跑测试都要在线下载几百 MB 的浏览器既慢又不稳定。安装完成后建议先跑一个最小脚本验证环境是否正常。以下是一个最简单的 Python 示例from playwright.sync_api import sync_playwright with sync_playwright() as p: browser p.chromium.launch(headlessFalse) page browser.new_page() page.goto(https://www.baidu.com) print(page.title()) browser.close()如果控制台能打印出页面标题说明 Playwright 安装成功、浏览器驱动没有问题。这一步我强烈建议大家先做不要直接跳到写大型框架否则后面排查环境问题会和业务问题混在一起很难收拾。2.2 语言选型Python 还是 TypeScript很多新手会纠结一个问题Playwright 的 Python 版本和 TypeScript 版本到底选哪个我的看法是没有绝对的优劣主要看团队基础和项目生态。如果你所在的团队一直用 Python 做测试pytest 生态非常成熟那选 Python 版肯定最顺手配合pytest-playwright插件几乎是无缝衔接。Python 版的定位是利用 Pytest 强大的断言和 fixture 体系用例组织起来非常灵活适合中小团队快速上手。如果你身处前端团队或者项目本身是 React/Vue 技术栈那 TypeScript 版更有优势。TS 版强类型支持能让你在写代码的时候就能发现参数错误而且 Playwright 官方对 TS 的支持也更早、更完整。还有一点TS 版可以天然利用 JavaScript 的page.evaluate()和浏览器交互更顺手复用前端的工具函数也更自然。实际项目里我见过不少混合使用的情况——前端写 TS 用例后端测试组用 Python。这不冲突Playwright 的协议层是语言无关的两边共享同一套浏览器二进制。唯一要注意的是不要在一个 repo 里混用两套语言管理同一批测试用例后续维护时 CI 脚本、依赖管理、报告聚合都会变得很乱。即使是跨团队也建议通过 API 通信或者统一输出格式来整合。2.3 同步模式还是异步模式Playwright 同时提供了同步 APIsync_api和异步 APIasync_api。在 Python 环境里默认推荐同步 API因为它更符合大多数测试人员的编码习惯而且是 pytest 生态的标准用法。但如果你要跑高并发任务或者想在同一个事件循环里混用其他异步库那异步 API 就有优势了。一个比较常见的场景是抓取大量页面数据时用asyncio.gather()并发打开多个 Playwright 页面效率可以提升好几倍。以下是一个异步 API 的并发示例import asyncio from playwright.async_api import async_playwright async def visit(url): async with async_playwright() as p: browser await p.chromium.launch() page await browser.new_page() await page.goto(url) title await page.title() await browser.close() return title async def main(): urls [https://www.a.com, https://www.b.com, https://www.c.com] tasks [visit(url) for url in urls] results await asyncio.gather(*tasks) for r in results: print(r) asyncio.run(main())这里有一个容易踩的坑异步模式下不能共享playwright实例给多个协程。每个协程都必须创建自己的async_playwright上下文否则会报错。这也是为什么我把async with async_playwright()写在每个协程内部的原因。如果你追求更极致的性能可以自己维护一个playwright实例和一个浏览器池但那是比较高级的用法项目不着急的话没必要一上来就整。3. 核心 API 与常用操作实战3.1 定位器Locator机制放弃复杂选择器Selenium 时代大家习惯用find_element(By.ID, xxx)、find_element(By.XPATH, xxx)来定位元素链式调用又长又难读。Playwright 的设计理念完全不同它把定位器当作一等公民你可以先定义一个目标元素的描述再去执行点击、输入、断言等操作。最常见的定位方式有这么几种page.get_by_role(button, name提交) # 按 ARIA 角色定位 page.get_by_label(用户名) # 按表单标签定位 page.get_by_placeholder(请输入密码) # 按占位符定位 page.get_by_text(注册) # 按可见文本定位 page.get_by_test_id(submit-btn) # 按>row page.locator(.user-table tr).filter(has_text张三) row.get_by_role(button, name删除).click()这意思很清楚先找到包含张三的那一行再点击该行内的删除按钮。逻辑清晰而且避免了一个非常常见的气泡事件误点问题——很多表格里每行都有操作按钮用 XPath 硬写很容易点到别的行。3.2 自动等待机制写干净测试的关键我在前面提过自动等待是 Playwright 最核心的杀手锏之一。这里稍微展开一下它的工作原理。Playwright 的操作会自动执行Actionability 检查。比如你调用page.click()时它会依次检查元素是否挂载到 DOM元素是否可见不是display: none、visibility: hidden、尺寸为 0元素是否稳定两次渲染之间没有位置变化元素是否可接收事件没有被其他元素遮挡元素是否处于启用状态不是 disabled只有所有检查都通过才会真正执行点击。这个机制非常强调用户体验——像真人一样去点击一个按钮而不是盲目地 send click event。基于这个特点你写测试的时候可以完全省略 Selenium 里常见的sleep和WebDriverWait代码量直接减少三分之一。那是不是完全没有必要写等待了也不是。个别场景还是需要主动等待的比如等待某个接口返回特定的数据、等待某个自定义事件触发。Playwright 也提供了对应的 APIpage.wait_for_url(**/order/success) page.wait_for_selector(.toast-success) page.expect_response(lambda response: response.url.startswith(/api/user) and response.status 200)这里要特别提醒自动等待并不是无等待。它只是替代了那些与元素状态相关的等待但如果你要等待某个异步逻辑完成后才能断言还是得用wait_for_*或者expect()关于expect()的用法我会在第四部分展开。3.3 多标签页、弹窗与文件下载现代 Web 应用里有几个高频场景是 Selenium 时代的噩梦点击链接后新标签页打开、浏览器原生弹窗、文件下载、文件上传。Playwright 都做了非常优雅的封装。先看多标签页的处理。Playwright 没有 Selenium 那种switch_to.window()的繁琐切换而是基于事件等待新页面出现with page.expect_popup() as info: page.click(a[target_blank]) new_page info.value new_page.wait_for_load_state() print(new_page.title())这个expect_popup()会在你点击后等待弹窗页加载完成然后返回新的Page对象。之后你想在旧页面还是新页面上操作完全取决于你把哪个 page 对象拿在手里逻辑很直观。弹窗alert/confirm/prompt的处理也类似用page.on(dialog, ...)注册监听器page.on(dialog, lambda dialog: dialog.accept()) page.click(button#show-dialog)文件下载更简单用expect_download()包住触发下载的点击操作然后拿到下载对象决定是保存还是直接丢弃with page.expect_download() as download_info: page.click(button#export-btn) download download_info.value download.save_as(/tmp/export_data.xlsx)上传文件的 API 同样直白page.set_input_files(input[typefile], path/to/file.xlsx)一次性搞定。这几个功能合在一起覆盖了一个典型 Web 业务系统 90% 以上的日常自动化需求。如果你是从 Selenium 迁移过来的你会明显感受到以前写工具方法才能实现的事情现在直接用 API 就行的爽快感。3.4 浏览器上下文BrowserContext到底怎么用BrowserContext 是 Playwright 中独有且非常重要的概念。你可以把它理解成一个隐形的用户会话每个 context 都有自己独立的 localStorage、sessionStorage、cookie、缓存、权限设置、设备信息UA、视口尺寸、地理位置等。为什么要大力引入这个概念因为 Web 应用绝大多数都和身份、状态、偏好绑定。你用一个浏览器登录了账号 A又想在同一个浏览器里测试账号 B如果共享 cookie 就会互相污染。Selenium 时代处理这个问题非常笨拙——要么新起一个 browser 实例要么手动 delete cookie要么启动不同的 profile。Playwright 则让你直接开出独立的平行空间互不干扰。典型用法如下context1 browser.new_context() page1 context1.new_page() page1.goto(https://example.com) page1.get_by_placeholder(用户名).fill(user_a) # ... 登录操作 context2 browser.new_context() page2 context2.new_page() page2.goto(https://example.com) # 这里是完全干净的会话和 context1 毫无关系更进阶一点的用法是复用已保存的登录状态。你可以先手动登录一次然后把 context 的 storage_state 保存下来之后每次跑测试直接加载这个状态免去每次都要走一遍登录流程# 首次运行保存登录态 context.storage_state(pathstate.json) # 后续测试直接加载 context browser.new_context(storage_statestate.json)这个特性在跑长流程用例时极其有用。比如你有 30 个用例都需要登录后才能操作如果每个用例都走一遍登录既慢又容易因为验证码、风控等因素失败。用 storage_state 直接跳过登录阶段整个执行时间能缩短一半以上。3.5 网络请求拦截与 Mock日常测试中经常需要模拟后端异常、修改接口响应、或者屏蔽外部的埋点请求。Playwright 提供了非常强大的网络层 API能在请求发出前或响应返回后介入。最简单的用法是拦截并取消某些请求page.route(**/analytics/*, lambda route: route.abort())略复杂一点是 mock 接口响应def mock_response(route): route.fulfill( status200, content_typeapplication/json, body{code: 0, data: {name: mock_user}} ) page.route(**/api/user/profile, mock_response)还有一个很实用的场景利用路由拦截把真实接口替换成一张本地图片或本地文件方便测试文件预览功能。比如测试 PDF 预览页面直接把接口返回拦截成body%PDF-1.4 ...不用依赖后端构造数据。在网络拦截这个环节我强烈建议写在 fixture 或 setup 阶段统一注册而不是散落在各个测试用例里否则后期你要是想统计全校验逻辑会发现mock 漫天飞很难定位哪条用例依赖了哪些 mock。4. 与 pytest 集成搭建一套可落地的自动化测试框架4.1 pytest-playwright 插件的正确打开方式Playwright 官方提供了pytest-playwright插件把浏览器、上下文、页面的生命周期管理和 pytest fixture 完美地结合在了一起。装完后你不需要自己写任何 setUp/tearDown 逻辑插件会自动为每一个测试用例创建一个独立的 browser context并在结束后自动关闭。基础用法相当简洁def test_login(page): page.goto(https://example.com/login) page.get_by_placeholder(邮箱).fill(testtest.com) page.get_by_placeholder(密码).fill(123456) page.get_by_role(button, name登录).click() page.wait_for_url(**/dashboard) assert page.url.endswith(/dashboard)这里的page是一个 pytest fixture它已经绑定到一个全新的、干净的上下文上。你只管写测试逻辑就行。如果需要调整浏览器类型、走 headless 模式或者启动参数通过 pytest 配置文件就能搞定。以pytest.ini为例[pytest] addopts -s -v --headed --browserchromium --base-urlhttps://staging.example.com testpaths ./tests其中--base-url是一个非常有用的选项它允许你在测试代码里只用相对路径page.goto(/login)换环境时只需要改一个配置项。4.2 fixture 设计从能跑到好维护的分水岭很多人刚接触 Playwright pytest 时开心地写完几条用例就跑上线。但只要用例一多你就会发现重复代码越来越多登录、创建数据、权限校验、环境清理...这时候就得靠 fixture 来治理了。下面这个 fixture 是我项目里的常用模式——创建一个已登录的页面对象pytest.fixture def logged_in_page(page): page.goto(/login) page.get_by_placeholder(邮箱).fill(cloudtest.com) page.get_by_placeholder(密码).fill(Abc123456) page.get_by_role(button, name登录).click() page.wait_for_url(**/dashboard) return page然后在测试函数里直接使用def test_dashboard_stats(logged_in_page): logged_in_page.get_by_text(今日订单量).wait_for() assert logged_in_page.locator(.stats-card).count() 0如果你的业务有一个创建项目的前置动作也可以写成带返回值且带资源的 fixturepytest.fixture def create_project(page, logged_in_page): def _create_project(name): logged_in_page.get_by_role(button, name新建项目).click() logged_in_page.get_by_placeholder(项目名称).fill(name) logged_in_page.get_by_role(button, name确认).click() logged_in_page.get_by_text(name).wait_for() return {name: name} return _create_project这种做法把造数和验数的逻辑集中在一个函数里后续如果前端交互改了只需要维护这一处 fixture所有依赖它的用例都不会散落改动。这就是 fixture 设计的核心价值让测试代码变薄让基础设施变厚。4.3 数据驱动与测试报告当用例开始规模化之后数据驱动基本是标配。最简单方式就是用 pytest 的参数化标记import pytest pytest.mark.parametrize(keyword, expect_result, [ (python, Python 官方教程), (playwright, Playwright 官方文档), (pytest, pytest 文档), ]) def test_search(page, keyword, expect_result): page.goto(https://www.baidu.com) page.get_by_placeholder(请输入关键词).fill(keyword) page.get_by_role(button, name百度一下).click() page.get_by_text(expect_result).first.wait_for()这样一组数据就能拆成多条独立用例而且每一条的执行结果都单独记录在报告中定位失败非常直观。报告方面pytest 生态里最常用的是pytest-html或者allure-pytest。如果你对报告的展示有更高要求我会推荐 Allure它支持历史趋势、标签分层、截图嵌入、数据驱动用例的自动合并展示看板效果很适合给管理层汇报。Playwright 自己也有强大的内置追踪功能运行用例时加上--tracingon会在失败时保存完整的 trace 文件。用npx playwright show-trace trace.zip打开后你可以看到每一步操作的 DOM 快照和网络请求这让定位失败原因变得无比轻松。5. 常见问题与排查技巧实录5.1 安装失败与浏览器下载缓慢的处理很多人在playwright install这一步卡住最常见的原因就是网络下载浏览器二进制文件超时。解决办法有很多比较靠谱的做法是使用国内镜像源或者让运维把浏览器二进制提前打包进 CI 镜像。这里给个最简单的解决方案PLAYWRIGHT_DOWNLOAD_HOSThttps://npmmirror.com/mirrors/playwright npx playwright install chromiumPLAYWRIGHT_DOWNLOAD_HOST是官方支持的镜像配置项更换下载源后速度提升非常明显。如果是在 Docker 环境里建议在 Dockerfile 里显式安装并缓存依赖而不是每次启动容器时再装。示例 Dockerfile 如下Python 环境FROM python:3.11-slim RUN apt-get update apt-get install -y \ libnss3 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libxcomposite1 \ libxdamage1 libxrandr2 libgbm1 libasound2 \ pip install playwright \ playwright install --with-deps chromium COPY . /app WORKDIR /app CMD [pytest, -v]把依赖和浏览器都打进镜像后续每次 CI 只跑代码执行力会快一个量级。5.2 测试不稳定超时、元素不可见、偶发失败自动化测试最大的敌人永远是不稳定。虽然 Playwright 的自动等待已经比 Selenium 好了无数倍但依然有几种经典坑需要特别注意。第一种页面有动画或渐入效果。如果元素在出现过程中持续改变位置或透明度Playwright 的稳定检查会反复等不到稳定状态导致超时。这种场景下可以手动指定更宽松的等待策略page.get_by_role(button, name保存).click( timeout15000, forceTrue )forceTrue会跳过可操作性检查直接执行点击。它适合用在你确认元素一定可以点击只是动画影响了下一次的稳定检查的场景像两层的定心丸。第二种接口响应慢但页面 UI 已渲染。比如页面进入时同时调三个接口页面上有几个统计卡片但统计卡片的数字是渲染完接口后才更新的。刚进入页面时断言卡片会出现初始值 0这时需要等待数字变化而不是等元素出现。建议用expect轮询断言from playwright.sync_api import expect page.goto(/dashboard) expect(page.locator(#order-count)).to_have_text(128)expect().to_have_text()会以轮询方式等待最多 5 秒默认期间不断检查文本是否变化。这个机制比assert page.locator(...).text_content() 128要鲁棒得多因为它自带重试。第三种页面元素有多个同名。表格、列表、卡片这些场景里经常会出现多个元素满足同一个选择器。用locator.first、locator.nth(i)或.filter()来精确定位不要靠盲猜get_by_text默认第一个。一个很实用的技巧是结合count()先断言数量是否大于 0再定位items page.locator(.inventory-item) assert items.count() 1 items.nth(2).get_by_role(button, name购买).click()5.3 定位器明明存在却失败如何及时展露真相遇到定位问题第一反应应该是看浏览器当前的 DOM 和网络请求而不是盲目修改选择器。Playwright 的 Trace Viewer 是排障神器开启方式是在运行脚本时加上--tracingonpytest --tracingon --trace-filetraces.zip跑完后用playwright show-trace traces.zip打开会看到一个时间轴每一步操作的截图、DOM、网络请求都列得清清楚楚。我能明显感觉到排查效率比 Selenium 时代提升了好几倍——以前靠自己加 log 或者截图现在直接回放整个会话。第二个用得比较多的调试技巧是page.screenshot()。可以在关键步骤前故意截图保存page.screenshot(pathf/tmp/debug_{int(time.time())}.png, full_pageTrue)如果配合 CI 的产物存储失败时自动把截图保留下来后续分析会省力很多。还有一个专门用于排查的 APIpage.locator(...).all_text_contents()和page.locator(...).count()。如果怀疑元素没有按预期渲染直接把这些结果打印出来比空口猜情况强太多。6. 性能优化与并行策略6.1 单测速度的手术刀上下文复用与浏览器复用刚上 Playwright 时最容易犯的错误是每一条用例都重新启动一个浏览器甚至重新启动 playwright 实例。这会干出非常夸张的资源浪费——本地跑测试时 CPU 打满、风扇狂转但整体耗时一点没降。Playwright 的设计里browser 是重量级资源建议全局只启动一次context 是轻量级隔离单位每个用例一个没问题。pytest-playwright 插件其实已经默认做了这个优化全局共享一个 browser每个测试用例单独分配新 context。所以除非你用裸 API 写脚本否则不太需要自己控制浏览器生命周期。但如果你用的是裸 API 写脚本比如爬虫或批量任务那就需要手动设计好了。一个合理的模式是整个任务启动一次playwright启动一个浏览器实例然后开多个 context 来做隔离减少重复启动浏览器的开销。with sync_playwright() as p: browser p.chromium.launch() contexts [] for i in range(5): contexts.append(browser.new_context()) # 并行处理多个任务...这个模式相比每个任务都启动浏览器性能提升大概在 3 到 5 倍左右尤其是在跑爬虫和批量数据采集时非常明显。6.2 pytest-xdist 并行执行算力换速度的典型方案Playwright pytest 项目想进一步提速最直接的方案是用pytest-xdist。它支持多进程并行执行测试用例。而由于每一个测试用例都使用独立的上下文并行时不需要担心数据冲突。安装很简单pip install pytest-xdist运行测试时指定参数pytest -n 4这里有两点经验需要强调第一不是线程数越多越快。并行执行时每个进程都会创建自己的浏览器实例4 个进程就是 4 个 Chromium 实例每个大约占用 300-500MB 内存。如果你的机器只有 8GB 内存建议-n 2就差不多了过高的并行度会引起内存交换反而变慢。第二并行模式下如果用例里写了pagefixture没问题插件帮你隔离得很干净。但如果用例之间共享了文件、数据库、外部服务就要小心了。比如你有 20 个用例同时向同一个测试环境的注册接口发送请求可能会触发服务端限流或者产生脏数据。这种场景下最好对公共资源做打标或者使用隔离环境。6.3 提升稳定性的进阶技巧重试与筛选再稳的框架也偶有翻车的时候合理利用重试可以极大减少 CI 误报。pytest 搭配pytest-rerunfailures使用pip install pytest-rerunfailures pytest --reruns 2 --reruns-delay 1但这里我要特别提醒重试不能滥用不要每次都把所有失败用例重跑三遍。重试只适用于确认是网络抖动或服务端临时异常的场景。如果是断言失败、定位失败这种业务性错误重试盖过了真实问题反而掩盖了 bug。建议在 CI 配置里对重试次数做一个成本控制组合策略是关键业务用例不重试稳定跑次要业务用例重试不超过两次。筛选策略上pytest 的-k参数和-m标记非常实用。比如只跑冒烟用例pytest -m smoke在用例上加标记pytest.mark.smoke def test_critical_login() ...这样按需筛选能保证大回归时先跑核心链路快速发现问题然后再跑全部用例。7. 从 Selenium 迁到 Playwright 的项目实战要点7.1 哪些项目适合现在迁移一个人人都关心的问题是到底要不要把现有 Selenium 项目迁移到 Playwright我的观点是——不要为了技术而技术先评估收益和成本。适合立刻迁移的项目有明显的特征用例多以 UI 工具为主并且 flaky 率很高比如每天都有几个点击无效、元素找不到的报错项目涉及大量 iframe、shadow DOM、弹窗、下载上传等能力团队对自动化维护投入疲于奔命有强烈的效率焦虑不适合立刻迁移的项目特征是当前项目几乎没有 UI 自动化从零起步团队已经基于 Selenium 沉淀了非常成熟的工具库和用例集迁移成本远大于收益团队缺乏足够的工程能力去处理 Playwright 引入后的新问题对于适合迁移的团队我建议用渐进式迁移而不是推倒重来。选一个风险相对低的模块比如用户注册流程或者订单查询流程先用 Playwright 重写 10~20 条用例跑两周看稳定率。如果数据明显优于 Selenium再逐步把核心链路的用例接过来。留一些老用例在新框架没完善时兜底这才是稳妥的做法。7.2 迁移过程中的代码改造与习惯调整从 Selenium 代码迁到 Playwright最大的改变不是 API 替换而是思维方式的转变。下面这张表是我整理的常见硬编码习惯对比场景Selenium 习惯Playwright 推荐等待元素WebDriverWait(...).until(EC.presence_of_element_located(...))直接用page.get_by_role(...)自带等待点击提交按钮driver.find_element(By.XPATH, //button[text()提交]).click()page.get_by_role(button, name提交).click()切换 iframedriver.switch_to.frame(...)page.frame_locator(...)或指定 locator 的frame属性新窗口处理driver.window_handles各种 switchpage.expect_popup()直接拿到新页面对象文件上传手敲send_keyspage.set_input_files(...)迁移时最容易翻车的地方是iframe 处理。Selenium 习惯是全局switch_to.frame()切来切去容易忘记切回来。Playwright 则是通过frame_locator精确定位 iframe 内的元素作用域局部化不会污染主文档的定位profile_iframe page.frame_locator(#profile-iframe) profile_iframe.get_by_placeholder(姓名).fill(张三)你不需要切出iframe所有对主文档的操作照常工作代码结构自然了很多。7.3 团队协作测试代码也是代码要按代码规范管Playwright 项目越做越大之后最核心的经验是把测试代码当成核心代码一样管理。哪怕只说整洁分包和命名也能大幅提升团队协作效率。建议遵循以下约定每个模块一个测试文件文件名为test_模块名.py页面交互封装在page_objects/目录下不直接写业务逻辑到用例里fixture 统一放在conftest.py里不要在单个用例文件中写常量数据统一放data/或config/文件用例里不硬编码断言尽量用 Playwright 的expect()不要用 Python 的assert加手写轮询拿页面对象模式来说它虽然会增加一点初期编码量但能带来极大的维护收益。一个登录页的封装如下class LoginPage: def __init__(self, page): self.page page self.email_input page.get_by_placeholder(邮箱) self.password_input page.get_by_placeholder(密码) self.submit_btn page.get_by_role(button, name登录) def login(self, email, password): self.email_input.fill(email) self.password_input.fill(password) self.submit_btn.click() self.page.wait_for_url(**/dashboard)用例层就只关心业务意图和 UI 细节解耦。前端改了样式不慌只改页面对象所有用例自然过渡。这样测试代码的复用性、可读性、可维护性都有了质的提升。8. 写在最后的一些个人建议我在实际项目中同时经历过 Selenium 和 Playwright 两代框架的团队落地若让我用一句话评价 Playwright不是它比 Selenium 快而是它让写 UI 自动化测试更像在写业务代码。自动等待、隔离上下文、trace、录制工具这些能力都是围绕降低心智负担来设计的。如果你打算在一个新项目里开始用 Playwright我建议第一周不要急着写框架先把官方文档的locator和actionability两章读明白再在本地用 Codegen 记录几个常见操作亲手感受一下自动等待带来的丝滑。这比直接抄一个别人的框架要扎实得多。还有一个常被忽略的小技巧在写用例前先写一个极简的 POC。拿你项目里最核心的一条用户路径比如登录 → 查列表 → 下载文件用 Playwright 直接写成一个脚本跑通并稳定重复十次。这段 POC 的关键在于验证三件事定位方式是否可靠、等待是否能覆盖所有异步、文件/弹窗等能力是否会碰到阻碍。POC 通过后再考虑搭建 pytest 框架、页面对象、CI 集成循序渐进远比一步到位稳妥。最后再分享一个我深有体会的经验自动化测试真正的瓶颈不在工具而在用例设计。Playwright 只能解决执行稳定的问题解决不了该测什么、怎么拆分、怎么设置断言的问题。所以无论工具怎么演进测试用例的结构化设计、场景覆盖度的思考、失败信息的可诊断性永远是最值得投入的部分。先把工具用好再把业务测透这条路走下去你的自动化测试项目就能真正成为团队的保障而不是负担。