Playwright实战指南:从Selenium迁移到CI集成的自动化测试实践
2026/9/7 2:56:23 网站建设 项目流程

Playwright 这几年在 Web 自动化领域的上升趋势非常明显。不管你是 Selenium 的老用户,还是刚准备入行写 UI 自动化,应该都听过一个说法:Playwright 正在取代 Selenium。我的判断是,这个说法在“新项目选型”这个维度上基本成立。Playwright 把等待、定位、多页面、网络拦截、调试这些过去最让人头疼的事情,都做进了框架底层。本文只写能落地的内容:环境怎么准备、第一个脚本怎么写、从 Selenium 迁移要注意什么、批量跑测和 CI 怎么接,以及最常见的报错怎么查。适合自动化测试工程师、开发自测任务,以及准备给现有项目换框架的团队。

1. 先确认一件事:说 Selenium 退出,到底是在说什么

1.1 为什么 Playwright 能成为新的默认选项

Selenium 不是不好,而是设计太老了。它的核心思想还是 WebDriver 协议那一套:脚本向浏览器发命令,浏览器回响应。这套设计稳定,但带来很多麻烦。比如你要先下载一个和浏览器版本严格匹配的驱动;比如元素没出现的时候,要自己写显式等待;比如打开多个标签页,要在window_handles里切来切去。

Playwright 走的是另一条路线。它默认采用事件驱动和自动等待,元素操作前框架会主动判断元素是否可见、是否可交互、是否被遮住。脚本里大量sleepWebDriverWait可以删掉。而且 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(headless=False, channel="chrome")

设置channel="chrome"时,Playwright 会寻找本机安装的 Chrome,不再使用自动下载的 Chromium 内核。好处是测试环境和用户真实浏览器一致,坏处是对本机浏览器版本有依赖,团队内部需要约定统一版本。

还有一个常见场景是内网环境下载不了浏览器。这时先确认是不是网络、磁盘或者依赖源配置问题,再让运维帮忙把浏览器安装包同步到内网。不要一遇到下载失败就去搜各种“免安装方案”,先把错误信息完整看一遍。我见过很多次所谓“Playwright 启动失败”,最后都只是浏览器没有装好。

2.3 用最短脚本验证环境

安装完成后,先跑一个最小脚本验证,不要直接写完整用例:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() page.goto("https://example.com") print(page.title()) browser.close()

如果弹出浏览器并打印出页面标题,说明环境和浏览器都正常。

在 Windows、macOS 本地开发时,headless=False方便观察;在 Linux CI 上跑,默认headless=True即无头模式。不要在本地和 CI 上使用完全相同的启动参数,本地调试要看得见,CI 要跑得快、不依赖桌面。

3. 第一个 Playwright 脚本:把 Selenium 的思路换过来

3.1 一个登录后搜索的完整例子

先看一个典型的用例:打开登录页,输入账号密码,点击登录,等待登录成功提示。

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False) 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 自动等待和断言:别再到处 sleep

Playwright 的自动等待默认超时是 30 秒。实际操作时,如果页面一直不满足条件,会抛出超时异常,而不是硬着头皮点一个不存在的按钮。这一点对 SPA 应用特别重要:很多前端渲染是异步的,元素出现、元素可点击、元素真正能响应点击,是三个不同状态。Playwright 的 actionability 检查就是把这三步统一封装了。

断言方面,建议直接用expect

from 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、textbox
  • get_by_placeholder:按输入框提示文字定位
  • get_by_label:按表单标签定位
  • get_by_title:按 title 属性定位
  • get_by_alt_text:按图片 alt 文本定位

实际项目里,我最常用的是get_by_roleget_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.frame

frame = 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# 等。

我的使用习惯是把它当成“定位器生成器”:

  1. 先在页面上点击、输入、跳转,把主流程录下来。
  2. 再用浏览器 DevTools 查看实际 DOM,确认录出来的 locator 能不能更稳定一些。
  3. 最后手动补断言、清理脏数据和前后置条件。

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( status=200, content_type="application/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(response=response, body=body) 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,安装后直接在测试函数里使用pagefixture:

pip install pytest-playwright
def 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-xdist

pip 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 --tracing=on

--tracing=on会记录用例执行的轨迹文件,失败时可以用 Playwright 的命令打开 Trace Viewer 回放,比只看截图更容易定位问题。

CI 上最容易遇到的是 Linux 系统库缺失。本地 Windows 能跑,一到 Linux 容器就报浏览器启动失败,多数是系统依赖库没装。建议在 CI 基础镜像执行:

playwright install --with-deps chromium

如果是 Docker 环境,还要确认浏览器沙箱相关配置。具体怎么启动要以你们团队的安全规范和官方容器镜像说明为准,不要为了图省事在不可信环境里随意关闭安全隔离。

7. 高频报错不要慌:按这套顺序查,一般十分钟能定位

7.1 几个高频报错的直接原因

报错信息常见原因怎么处理
Target closedcontext 或 page 被提前关闭,还在继续调用检查对象作用域,不要在with结束后再用 page
Timeout 30000ms exceeded元素没出现、不可操作或 locator 写错先用page.locator(...).count()确认元素存在,再检查定位是否匹配
Strict mode violationlocator 匹配到多个元素filter.nth().first收窄范围
Executable doesn't exist浏览器内核没下载执行playwright install chromium
启动时缺系统库Linux 环境依赖不完整执行playwright install --with-deps
沙箱相关报错Docker 或特定权限环境下运行检查 CI 容器配置和安全规范

7.2 定位器超时不是马上改代码

遇到超时,第一反应不应该是把默认超时从 30 秒改成 60 秒,而是先问几个问题:

  1. 这个元素是不是真的存在?直接在浏览器里手动打开对应页面,用 DevTools 搜索文本或 selector。
  2. 它是不是在 iframe 里?在页面里看 DOM 树,元素全部包裹在某个 iframe 里,那就在frame_locator下定位。
  3. 它是不是有多个匹配?很多按钮在不同弹窗里名字一样,用严格模式确认。
  4. 它是不是异步渲染?如果是,Playwright 的自动等待会解决,不用手工 sleep。

如果这些都没问题,再把定位器写得更精确。比如一个“确定”按钮可能在弹窗、抽屉、页面本身都有,那就先从页面里找弹窗的父容器,再在容器内定位。

7.3 一套可以复用的排查链路

我会按这个顺序处理问题:

  1. 看现象:是启动失败、定位超时、断言失败,还是页面崩溃?
  2. 看浏览器:本机手动打开这个页面,走到同样操作,页面是否正常?
  3. 看环境:本地能跑、CI 不能跑,先对比系统依赖和浏览器版本。
  4. 看日志:Playwright 报错信息里通常会带上 locator 表达式和等待过程,仔细读最后几行。
  5. 看修改记录:昨天能跑、今天挂了,先看页面是否改版,再回滚测试代码验证。

这套顺序比一上来就搜报错更高效。很多所谓“框架不稳定”,最后都是页面改了、数据被污染、或者 locator 匹配到了多个元素。

8. Selenium 还有哪些场景要留,以及怎么把切换做成增量工程

8.1 不一定立刻迁移的几种情况

虽然 Playwright 在大多数新项目里更有优势,但下面几种情况不建议硬迁:

  • 存量 Selenium 用例量很大,团队短期内没有精力重写。
  • 测试基建已经深度绑定 Selenium Grid、远程 WebDriver 或某类商业测试平台。
  • 有旧版浏览器或 IE 兼容需求,Playwright 并不支持老旧 IE 生态。
  • 团队主要维护的是几十条简单回归用例,重写收益不高。

技术选型最忌讳“因为大家都说好就重写”。更好的做法是选一个稳定的业务模块,用 Playwright 做试点,跑一段时间看稳定性、维护成本和执行效率,用数据说话。

8.2 切换建议:增量试点而不是推倒重来

我建议分四步切换:

  1. 选一条用户主路径,比如“登录 -> 列表 -> 详情 -> 下单”,用 Playwright 重写。
  2. 和现有 Selenium 用例并行跑一周,对比失败率和耗时。
  3. 稳定后,让团队新用例默认用 Playwright 写。
  4. 老用例逐步迁移,迁移优先级按业务重要程度和失败率决定。

切框架最大的成本不是 API 差异,而是之前积累的测试设计思路。Selenium 时代常用“先等待、再操作、再验证”的思路没有变,只是 Playwright 把等待内化到了操作里。

8.3 学习路线和最后一个建议

如果现在准备系统地学习 Playwright,我的建议顺序是:

  1. 先把 locator 体系和自动等待彻底搞明白,这是地基。
  2. 写通一条完整业务链路的用例,包含页面跳转、表单输入、按钮操作、断言。
  3. 学会用 codegen 和 Trace Viewer,这两个工具能节省大量定位和调试时间。
  4. 再学网络拦截、多 context 隔离和 pytest 集成。
  5. 最后再碰并发、CI 和 Docker。

看官方文档时,如果英文吃力,可以找社区维护的 Playwright 中文手册辅助阅读,但接口签名和参数以官方英文文档为准。另外,如果你在用 AI 编程工具,现在也有 AI 编码助手支持 Playwright 的 MCP 集成,可以让模型辅助操作浏览器、复现前端问题。这个方向还在快速变化,建议先把 locator、context、page 这些基础概念掌握,再去看 MCP 相关配置。

很多人学了一半,突然跑去研究怎么让自动化脚本不被网站识别。这个话题我不建议花精力,尤其是对别人的网站。做正常测试,你只需要确保脚本稳定、数据隔离、用例可维护,这些能力比任何“绕过检测”的技巧都值钱。

最后留一句实操经验:新项目切 Playwright 后,稳定执行的关键不是框架选择,而是测试数据隔离和失败可观测。把 context 会话、账号数据和 trace 记录提前设计好,比纠结一两个 API 方法重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询