Playwright 自动化测试实战:从选型到稳定落地的完整路径

发布时间:2026/10/20 9:07:40
Playwright 自动化测试实战:从选型到稳定落地的完整路径
最近把组里的端到端测试整套迁移到了 Playwright折腾了一个多月跑通了核心流程也踩了不少坑。回头看看这个选择其实很值得聊一聊。标题叫“Playwright 自动化测试学习”但我想写的不是官方文档翻译而是一个实际搞过项目的人从选型到落地、从踩坑到稳定的完整路径。如果你正准备引入一个新框架或者正在犹豫要不要把老的 Selenium 脚本重写一遍这篇文章应该能帮你少走不少弯路。先说结论Playwright 确实值得学而且它的设计思路对测试开发这件事本身就有很大影响。我自己从它身上学到的不只是 API 怎么调更多的是“怎么把低价值、高脆弱的脚本变成稳定、可维护、真正能守住质量的资产”。1. 为什么选 Playwright框架选型背后的逻辑1.1 经历过 Selenium 的人都懂我自己是 Selenium 老用户从 WebDriver 时代开始写项目里维护了几百条用例。说实话Selenium 能用但用得很辛苦。最大的问题不是功能不够而是“等待”这件事需要手动管理。你要么到处塞Thread.sleep()要么自己封装一个 WebDriverWait不断试错显式等待条件。到了 CI 上最常见的结果就是——昨晚上还好好的用例今天早上红了一片点开日志大概率是“element not interactable”或者超时。这不是脚本写错了而是页面渲染变慢了、某个接口延迟了、或者弹窗遮挡了元素。Selenium 的模型是“你告诉浏览器做什么”至于做的时候元素在不在、能不能点它不管一旦操作失败就抛异常。所以开发者需要自己去解决时序问题这其实是把框架的短板转嫁给了使用者。1.2 Cypress 的进步与局限后来社区里 Cypress 火过一阵它的设计理念确实先进自动重试、内置断言、开发者体验好还提供了时间旅行式的调试体验。如果你完全在 Node 生态内、只测浏览器内的行为Cypress 是很舒服的。但我在实际项目里遇到了几道坎。第一Cypress 建立在它自己的命令队列机制上很多现代前端框架的非标准交互尤其是多标签页、新开窗口、跨域 iframe 这类场景支持起来很别扭。第二它的架构是运行在浏览器内很多功能受限于同源策略做接口 mock 和网络层控制不如预期灵活。第三也是最重要的——当我同时要跑 WebKit 内核的兼容性回归时Cypress 的支持一直比较弱。1.3 Playwright 的破局点在哪Playwright 的底层是直接基于浏览器调试协议CDP工作的这意味着它跟浏览器之间是“原生”的服务关系而不是像 Selenium 那样通过中间协议转发指令。它对浏览器内部控制力强得多等待机制、网络事件、渲染状态都能感知。它把 Selenium 最难的显式等待变成了默认行为。比如click()之前Playwright 会自己去确认元素“已附加到 DOM、可见、稳定、可接收事件”满足条件才真正点击否则一直等待直到超时。这不是简单的sleep而是一套有状态的智能等待策略。用一句话总结Playwright 把“尽力而为”的自动化变成了“面向状态”的自动化。这也是我最终选择它的核心原因。2. 环境准备与第一份测试代码2.1 安装和浏览器内核管理我项目用的是 Node.js 生态所以下面的命令基本是npm体系。Python 版的pytest-playwright原理类似选择哪种取决于团队技术栈。npm init playwrightlatest这个命令会帮你初始化测试项目结构默认生成一个tests目录和playwright.config.ts配置文件。注意它不会自动下载浏览器需要单独执行npx playwright install这条命令会下载 Chromium、Firefox、WebKit 三个浏览器内核体积不小。如果你只想跑 Chromium可以加参数npx playwright install chromium在 Linux CI 环境上还要装系统依赖npx playwright install-deps很多人在 Docker 或 Jenkins 里跑不起来八成是漏了这一步。系统缺了底层动态库浏览器起不来报错信息又往往很隐晦。2.2 配置文件的几个关键项playwright.config.ts是测试的“总闸”理解和调好它比写测试代码更重要。我最常调整的几项import { defineConfig } from playwright/test; export default defineConfig({ testDir: ./tests, timeout: 30000, expect: { timeout: 5000 }, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 4 : undefined, use: { baseURL: https://example.test.com, headless: true, viewport: { width: 1280, height: 720 }, screenshot: only-on-failure, trace: retain-on-failure }, projects: [ { name: chromium, use: { browserName: chromium } }, { name: firefox, use: { browserName: firefox } } ] });几个容易被忽略的点expect.timeout和全局timeout是两回事。全局超时管的是整个测试用例的执行时间断言超时管的是单个expect的等待时间。我习惯把断言超时调小一点这样元素迟迟不出现时能更快失败、更快暴露问题。trace是 Playwright 的杀手级功能建议始终打开失败自动保留。调试时相当于有了一台“时空机”后面会细说。retries在本地关掉在 CI 开 2 次避免偶发网络抖动把整晚的回归全染红。2.3 先用 Codegen 写出第一个用例很多人一上来就手写选择器其实没必要。Playwright 提供了codegen模式可以在图形界面里操作浏览器同时自动生成脚本。这是学习门槛最低的入口。npx playwright codegen https://example.test.com/login执行后浏览器窗口和一个脚本录制面板会同时出现。你在页面上输入账号、密码、点登录代码就一行行生成。生成完复制到测试文件里稍微整理一下就是一条用例。我用这个功能带过不少刚接触自动化测试的同事基本 10 分钟就能跑通第一个用例。先理解框架怎么“理解”你的操作再动手去写复杂逻辑会顺畅很多。3. 核心 API 的理解与实战3.1 三个级别Browser、Context、PagePlaywright 里最核心的三个对象是Browser、BrowserContext、Page。技术文档写得很清楚但很多人没意识到“隔离”这件事的价值。Browser就是浏览器进程实例比如chromium.launch()得到的对象。BrowserContext是一个独立的浏览器会话相当于一个隐身窗口。在这个 context 里设置的 cookie、localStorage、权限不会影响其他 context。Page则是 context 里的一张标签页。这种分层设计带来的直接好处是测试天然隔离。我跑一个涉及登录态的用例时只需要在 beforeEach 里创建一个新 context再在新 page 上操作。不需要清 cookie、不需要 reload每个用例都是“白纸状态”。import { test, chromium } from playwright/test; test(独立的 context 互不干扰, async () { const browser await chromium.launch(); const contextA await browser.newContext(); const contextB await browser.newContext(); const pageA await contextA.newPage(); const pageB await contextB.newPage(); await pageA.goto(https://example.test.com); await contextA.addCookies([{ name: token, value: A, url: https://example.test.com }]); // pageB 拿不到 pageA 的 cookie因为 context 是隔离的 const cookiesB await contextB.cookies(); });如果从 Selenium 迁移过来你会明显感觉“隔离性”这件事不再需要自己操心了。以前多个测试类之间搞不好就串了 session现在 context 一开物理隔离。3.2 Locator 与自动等待脚本变稳的核心Locator是 Playwright 里最重要的概念但很多新手理解成“就是一个查找元素的 API”其实不止。Locator 是惰性求值的它解析选择器的动作不会立刻发生而是等到你调用.click()、.fill()、.textContent()这些动作时才真正去 DOM 里找。加上所有动作都有自动等待所以在脚本里写page.locator(button).click()时Playwright 会轮询直到按钮满足可点击条件。自动等待的具体条件很有意思它是组合式的元素在 DOM 里存在元素可见有非零尺寸且没有visibility: hidden元素稳定连续两次动画帧位置不变元素可接收事件没有被遮挡元素可编辑对输入类操作适用正因为有这套机制我重写 Selenium 用例时删掉了 80% 以上的等待逻辑。这里想提醒的是不要在任何地方主动加waitForTimeout。除非你在验证极特殊场景否则主动 sleep 反而会破坏 smart waiting 的自动判断。你 sleep 一秒钟页面可能两秒后才加载完但你 sleep 三秒钟又等于把不稳定的锅甩给了等待时间本身。相信我删掉所有 sleep 后用例稳定性是上升的。3.3 选择器策略从脆弱到稳健选择器是测试稳定性的大头。XPath 写起来方便但页面前端加一个 div、改一层结构就挂。我的经验是优先用用户可感知的属性定位而不是结构。Playwright 内置了丰富的定位语义// 推荐用 Role 定位接近用户视角 await page.getByRole(button, { name: 提交 }).click(); // 推荐用 Label 定位表单 await page.getByLabel(用户名).fill(test); // 推荐用 TestId 定位非语义化元素 await page.getByTestId(submit-btn).click(); // 不推荐XPath 依赖 DOM 结构 await page.locator(//div[classform]/div[2]/button).click();用getByRole有个额外的好处它顺带校验了可访问性。某个按钮明明叫“提交”但页面上却有两个同名的脚本就会报“strict mode violation”提示你定位不唯一。这能反过来帮你发现前端的语义问题。如果元素既没有可读文本也没有合适的 role那就要求前端在代码里埋>import { test as base, expect } from playwright/test; // 扩展一个带登录状态的 fixture export const test base.extend({ loggedInPage: async ({ page }, use) { // 通用登录逻辑 await page.goto(https://example.test.com/login); await page.getByLabel(用户名).fill(tester); await page.getByLabel(密码).fill(password); await page.getByRole(button, { name: 登录 }).click(); await page.waitForURL(**/dashboard); // 把已登录的 page 传给测试用例 await use(page); } }); test(已登录状态下的个人中心, async ({ loggedInPage }) { await loggedInPage.goto(https://example.test.com/profile); await expect(loggedInPage.getByText(我的订单)).toBeVisible(); });还有一个更高效的做法就是先用storageState保存登录态。第一次登录后Playwright 会把 cookie 和 localStorage 存成一个 JSON 文件后面所有测试直接加载这个状态不需要再走一遍登录流程。// 第一次运行登录并保存状态 await page.context().storageState({ path: auth.json }); // 后续运行直接复用 const context await browser.newContext({ storageState: auth.json });这个方案在跑大型回归时提速非常明显。但我踩过坑如果后端 session 有过期时间比如 2 小时那保存的存储态在 CI 长任务里会失效。解决方案是写一个自动刷新逻辑检测到跳转登录页就重新走一遍登录流程刷新存储状态再继续。4.2 并行执行与 CI 集成Playwright 默认就可以多 worker 并发跑。配置项workers控制并发数每个 worker 跑自己的浏览器实例。我本机 16 核跑 300 条用例大概 6 分钟如果串行接近 40 分钟。差距就是这么大。但并行也有代价——你要确保测试之间没有资源冲突。比如多个用例同时创建同名订单数据库就会撞。我的建议是核心业务数据要在用例内部构造不要依赖外部预置数据用 API 调用在测试开始前去创建一份属于这条用例自己的数据。这样并行天然安全。CI 集成方面我用的是很常见的 GitLab CI Docker 镜像方案。Dockerfile 里提前装好浏览器依赖跑任务时直接执行npx playwright test --reporterhtml --reporterdot报告上传为 CI artifact失败时可以点开 HTML 报告直接看录制视频和失败截图。这里有个细节如果镜像里不缓存浏览器每次 CI 都重新下载耗时会非常难看。我把npx playwright install放进镜像构建阶段而不是每次跑测试时再执行。4.3 覆盖几个高频场景上传下载、多标签页、iframe实际项目里除了点击输入还有不少“绕不过去”的场景这里把我验证过的方案写下来。文件上传Playwright 处理得比很多框架直接。直接用setInputFiles就能喂文件不需要打开系统文件选择框。await page.getByLabel(上传附件).setInputFiles({ name: report.pdf, mimeType: application/pdf, buffer: Buffer.from(fake pdf content) });也可以不传 buffer直接传本地文件路径await page.getByLabel(上传附件).setInputFiles(tests/fixtures/sample.pdf);文件下载重点在于download事件可能要过一会儿才触发。正确姿势是同时等待事件和点击动作用Promise.all。const downloadPromise page.waitForEvent(download); await page.getByRole(button, { name: 导出数据 }).click(); const download await downloadPromise; const path await download.path();多标签页点击“在新标签页打开”这类操作后页面跳到了一个全新的 Page 对象。最稳的处理方式不是context.pages()去猜而是等待事件。const pagePromise context.waitForEvent(page); await page.getByRole(link, { name: 新窗口打开 }).click(); const newPage await pagePromise; await newPage.getByText(新页面内容).waitFor();iframe登录流程最容易遇到 iframe。Playwright 的 frame locator 写起来直接const frame page.frameLocator(#login-iframe); await frame.getByLabel(手机号).fill(13800000000); await frame.getByRole(button, { name: 获取验证码 }).click();这里有一个从 Selenium 迁过来的人常见误区frame 一旦被页面重新渲染原来的frameLocator会失效需要重新获取。不要在循环里复用同一个 frameLocator 对象每次操作前重新定位一次最保险。5. 网络拦截、Trace 与调试进阶三板斧5.1 拦截与 Mock前端不再等后端E2E 测试最大的痛点之一是依赖后端接口稳定。后端一抖前端测试就跟着红。Playwright 的路由拦截能把这个依赖从根上切掉。await page.route(**/api/order/list, route { // 攔截请求伪造想要的响应 route.fulfill({ status: 200, contentType: application/json, body: JSON.stringify({ code: 0, data: { list: [] } }) }); });也可以把请求重定向到本地 mock 服务await page.route(**/api/user/**, route route.continue({ url: https://localhost:8080/mock/ route.request().url().split(/).pop() }));这个能力解放了我很大一部分工作。前端联调后端之前我就可以先把所有接口 mock 成预期值把所有 UI 流程跑通。等后端真的联调时再把路由拦截摘掉回归一遍真实链路定位问题会快很多。5.2 Trace Viewer测试界的时空机Trace Viewer 是我向所有同事推荐的第一功能。启动 trace 后Playwright 会把测试过程中的每一步操作、DOM 快照、网络请求、控制台日志、截图全部记录下来。在配置里开启use: { trace: retain-on-failure }也可以手工指定某一条用例完整记录test(需要重点排查的用例, async ({ page }, testInfo) { await page.context().tracing.start({ screenshots: true, snapshots: true }); // ... 测试逻辑 ... await page.context().tracing.stop({ path: trace-${testInfo.title}.zip }); });跑完后用命令行打开npx playwright show-trace trace-file.zip在界面上你可以拖动时间轴看到每一步之前元素是什么状态点击动作发生的那一刻 DOM 长什么样。有一次排查“偶发点击失效”我通过 trace 发现在点击按钮的前一步页面弹出了一个不在预期里的浮层把按钮盖住了。这在普通日志里完全看不出原因但 trace 一眼就锁定了问题。5.3 CI 里的稳定性工程化手段测试上 CI 之后稳定性不完全是“脚本写得好不好”的问题还涉及很多环境因素。我沉淀了几个非常有效的实操手段。第一失败自动重试但别一个用例无限重试。我在 CI 开retries: 2这个次数能滤掉偶发的资源加载延迟又不会因为重试掩盖真实的逻辑缺陷。第二失败自动产物收集。配置screenshot: only-on-failure和trace: retain-on-failure再配合视频录制use: { video: retain-on-failure }这样每次 CI 失败我都有一整套“案发现场”截图、视频、DOM 快照、网络面板。解决问题时间从小时级下降到分钟级。第三串行执行特定敏感用例。有些业务场景对并发不友好比如支付回调、金额校验。这时可以给这些用例打上特殊 tag在 CI pipeline 里分两条任务普通用例并行跑敏感用例单独串行跑。这是性价比很高的取舍。6. 常见问题与排查速查表6.1 超时类问题现象TimeoutError: page.goto: timeout 30000ms exceeded排查步骤先确认是不是网络问题——打开 trace看请求状态码DNS 解析是否正常确认页面是否有重定向地狱——短链跳长链再跳登录页goto 会一直等检查是否需要服务端渲染——如果页面 JS 在headless模式执行有问题同样会超时常见解法合理放大timeout用waitUntil: domcontentloaded替代默认的load把goto和后续断言的等待职责分开让 goto 只负责导航完成6.2 环境与运行时报错现象Error: browserType.launch: Executable doesnt exist at ...这是浏览器没装好。本地跑npx playwright installCI 检查 Docker 镜像构建日志。现象Target page, context or browser has been closed这是操作一个已经关闭的页面对象。常见于多标签页管理原页面点了链接后newPage出现了但原 page 被服务端重定向或被前端关闭。排查时不盯着报错行而是顺着 trace 看页面生命周期。现象Element is not attached to the DOM这是页面 DOM 被前端框架更新了。元素之前定位到了执行动作前被重新渲染导致引用失效。解法是不要缓存长时间存活的 locator每次操作前重新定位或者用locator.click()自带的自动等待重新发现元素。6.3 稳定性排查流程我内部整理了一套排查“flaky test”的流程自己也一直在用第一步看有没有主动 sleep。有就先全删掉。第二步开 trace观察失败发生在哪一步操作前一步页面状态是什么。第三步检查网络请求——失败前是否有超时、慢响应、非预期重定向。第四步检查选择器——是否命中了多个元素命中的元素是否会被动画影响位置。第五步确认测试数据是否被并行测试污染。快 80% 的“偶发失败”跑完这五步都有答案。剩下的 20%大概率是后端偶发 5xx、第三方服务抖动这类可以在 CI 上重试消化不用过度投入人力去追。7. 一点个人感受正式把 Playwright 落地到项目之后我最大的体会是自动化测试的维护成本主要不取决于你用哪个框架而取决于定位元素的方式和等待策略的合理性。Playwright 把等待这件事从“开发者的责任”变成了“框架的默认能力”这个转变直接降低了团队的写作门槛。组里第 3 天就有新人提交了第一条像样的 E2E 用例放在 Selenium 时代这个周期至少是两周。我还会建议大家从现有业务里挑一个“频繁回归、每次都要花半小时手工点”的流程比如订单发货、发票申请这种用 Codegen 先跑通一遍。你会在第一个用例跑通的时候直观感受到“手点十分钟、脚本点十秒”那种爽感。然后你会和我一样把越来越多的核心流程交给它去守。测试代码也是代码需要 review、需要重构、需要关注长期维护成本。Playwright 给了我们一套很好的基础工具真正能让它发挥价值的还是你对业务的理解和对“什么值得测”的判断力。