ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Playwright实战:告别不稳定的自动化测试,从入门到CI集成

Playwright实战:告别不稳定的自动化测试,从入门到CI集成

1. 为什么我抛弃了旧的自动化方案,转投 Playwright 阵营

先交代一下背景。我之前的项目里大量使用老牌的自动化测试框架做端到端测试,维护成本是真的高。每次跑测试,最头疼的不是业务逻辑变复杂,而是脚本和浏览器之间的“同步问题”:明明页面元素已经渲染出来了,脚本还在傻等;偶尔网络慢一点,用例就大面积误报。团队里每个人都曾被这种“不稳定”折磨过,最后大家甚至对自动化测试产生了某种抵触情绪。

后来我开始调研新一代的自动化测试工具,注意到 Playwright 出现的频率越来越高。GitHub 上星标涨得飞快,核心团队是从老牌框架项目出来的,属于“原生血统”的正规军。最初我抱着试试看的心态在几个小项目里跑了一下,结果发现几个痛点被精准解决了:

  • 内置了自动等待机制,再也不用到处写sleep(3000)或者是轮询轮询再轮询。
  • 一套 API 同时覆盖 Chromium、Firefox、WebKit 三种浏览器内核,跨浏览器验证不再是奢望。
  • 默认采用浏览器上下文隔离,每个测试用例天然独立,互相不污染。
  • 自带Trace Viewer(追踪查看器),用例挂了能看到完整录屏、DOM 快照和网络请求,定位问题像看回放一样。

这几个特性几乎是在对着我之前踩过的坑逐一开火。所以这期内容我不打算只写一堆 API 文档翻译,而是想按照我自己的学习路径和实战经验,把 Playwright 从入门到落地整个流程串一遍。内容覆盖环境搭建、核心概念、常用 API、断言设计、调试技巧、测试报告以及持续集成接入,尽量做到“看完就能在自己项目里用起来”。

如果你正在做 Web 自动化测试,或者刚接触 Playwright 想快速进入实战状态,这篇文章应该能帮你省掉不少摸索时间。如果你对老框架已经很熟,想找个更稳的替代方案,这篇内容也可以给你一个相对客观的参考。

2. 环境准备与第一个测试用例:五分钟先跑起来

2.1 安装与内置浏览器管理

Playwright 的安装过程比我预想中还要顺。项目里只需要基于 Node.js(版本建议 16 以上),执行一条命令就能把核心库装好:

npm init -y npm install -D @playwright/test

很多人会问,装完包是不是还得单独下载 Chrome 或者 Firefox?实际上 Playwright 会自己维护一套专用浏览器版本,你需要再执行一行命令把浏览器内核拉下来:

npx playwright install

这里它默认会同时下载 Chromium、Firefox 和 WebKit 三件套。如果你只做业务上的回归测试,只下 Chromium 也能跑,但下载全量版本有个好处:跨浏览器兼容性验证可以随时做,不耽误事。

注意:npx playwright install下载的浏览器路径和系统里你自己装的 Chrome 不是同一个东西,它们的版本是配套的,互不干扰。

2.2 第一个冒烟测试脚本

装完依赖之后,我习惯先在项目根目录建一个tests文件夹,然后丢一个最简单的冒烟脚本进去:

const { test, expect } = require('@playwright/test'); test('第一个冒烟用例', async ({ page }) => { await page.goto('https://example.com'); await expect(page).toHaveTitle('Example Domain'); });

这里面的page对象是由框架自动注入的,每个测试都会拿到一个全新的页面实例,你不需要关心它的创建和销毁过程。然后执行:

npx playwright test

框架会自动找到tests目录下的所有.spec.js或者.test.js文件,逐个跑完并输出一个美观的测试报告。

刚开始跑这个用例,就能感受到 exec 速度还不错。默认情况下,Playwright 是并行执行测试文件的,如果测试文件多了,它还会自动分配 worker 进程,尽量吃满 CPU 资源。

2.3 为什么 Playwright 能让脚本“更稳”

这里想多说一句,因为很多人用老框架写自动化,最后脚本全部变成了“睡眠大作战”:sleep(1秒)、sleep(2秒)、循环重试……这样写出来的脚本不但慢,而且特别脆。Playwright 之所以“稳”,核心在于它内置了一套可操作性的检查机制(Actionability Checks)。

在点击、填写、选择这类操作之前,它自动会做几件事:

  1. 元素是否已经附着在 DOM 上。
  2. 元素是否可见,不是隐藏状态。
  3. 元素是否稳定(比如动画还没停,它会等着)。
  4. 元素是否接收事件(比如有没有被遮罩层挡住)。
  5. 元素是否可用(不是 disabled 状态)。

这五条检查全部通过以后,操作才会真正执行。如果过了一段时间仍然不满足,才会报超时错误。这等于把以前大家手动写的“等待逻辑”内化到了框架层,脚本稳定性和开发效率一下子提升了一个档次。

3. 核心概念与定位器机制:不懂这些东西后面很容易踩坑

3.1 页面对象与浏览器上下文

在正式写复杂用例之前,有必要把 Playwright 的几个核心概念理清楚。

  • Browser:浏览器实例,对应一个完整的浏览器进程。
  • BrowserContext:浏览器上下文,可以简单理解成一个独立的“隐身窗口”会话。不同 context 之间的 Cookie、缓存、本地存储都是隔离的。
  • Page:页面对象,表示一个标签页。大多数情况下,你的操作都是围绕 page 展开的。

这里最关键的设计是 BrowserContext。测试用例最怕什么?最怕相互影响。A 用例登录了,B 用例还是游客状态;A 用例往 localStorage 写了配置,B 用例读到了脏数据。在 Playwright 里,每个测试默认都跑在独立的 context 里,从源头上隔离了干扰。这也是我当初决定深入使用它的一个重要理由。

框架还给每个测试提供了固定的pagefixture,你甚至不用自己手动创建 context,直接拿去用即可。

3.2 定位器:从 CSS 选择器到逻辑定位

早期自动化测试里,大家习惯用page.click('#btn-submit')这类的写法,选择器写在操作里。这种写法最大的问题是改起来麻烦,代码里选择器到处散落,后期维护基本靠全局搜索。

Playwright 推荐的方式是定位器(Locator)。定位器不是立即去页面上找元素,而是先描述“我要找什么”,等到真正要执行操作或者断言时,再由框架去动态匹配。这个概念和函数式编程里的“惰性求值”有点像。

举个例子,原来我们可能会这样处理复选框:

await page.check('input[type="checkbox"]');

但更好的方式是把定位器抽象出来:

const agreeCheckbox = page.locator('input[type="checkbox"]'); await agreeCheckbox.check();

这样不仅代码可读性更高,也方便复用。Playwright 同时也提供了一组语义更强的定位器方法,比如getByRole()、getByText()、getByLabel()。在可访问性做得比较好的页面上,我更喜欢用角色定位器,例如:

await page.getByRole('button', { name: '提交订单' }).click();

这个方法接近用户真实的使用视角——用户看到一个按钮,看到按钮上的文字,然后点击。脚本也按这个思路去找目标元素,别想着非得写一大长串 div 嵌套层级。

3.3 定位器优先级原则

很多新手上来就喜欢写一气呵成的 CSS 选择器,什么#root div.container > div.row > button.btn-primary,这种选择器的问题在于一旦页面结构改版,比如多加一层 div,用例就全挂了。我自己总结了一个优先级的参考顺序,可以给大家一个方向:

优先级定位方式适用场景
1用户可见文本链接、按钮、标题
2无障碍角色对语义标签支持良好的项目
3Label 标签表单相关的输入控件
4表单占位符输入框有 placeholder 提示
5测试专用 ID>await expect(page.getByText('保存成功')).toBeVisible();

这个断言会在超时前不断轮询,直到元素可见或者超时。比原来“睡眠固定时间”可靠得多。这个设计核心就是:给条件断言一个时间窗口,让它在窗口内反复检查,而不是盲等。

4.2 什么时候需要手动等待

虽然框架很聪明,但有些场景还是需要一点手动控制的。比如你要等某个接口返回之后,才能确定后续数据是否完整;或者某个元素被隐藏后,页面布局变化把另一个元素推到了新的位置。这类“时序敏感”的场景,建议有两种姿势:

第一种,是等待特定接口响应:

const responsePromise = page.waitForResponse('**/api/order/list'); await page.click('button:has-text("刷新")'); const response = await responsePromise; console.log(await response.json());

这样写的好处是把“等待”和“业务”绑定在一起,而不是干等时间。第二种,是显式等待某个元素状态,比如:

await page.waitForSelector('.loading-hidden', { state: 'hidden' });

但总体上我的原则是:能靠 expect 自动重试解决的,就别动 wait 相关 API;能靠 waitForResponse 解决的,就别写固定 sleep。固定时间等待永远是最后选项,因为它在机器负载高或网络波动时,一定会反过来坑你。

4.3 超时配置的合理设定

超时时间大家也可以按项目调一下。默认的 expect 超时是 5 秒,如果你在很慢的页面或者复杂渲染场景下,可以按需改到 15 秒甚至更长。我喜欢在配置文件playwright.config.js里统一管理:

module.exports = { timeout: 30000, expect: { timeout: 10000 } };

但注意,不要让超时时间给脚本“兜底”。如果一个断言每次都是等到超时才通过,说明前面操作可能不太稳,这种隐藏风险越早暴露越好,别只用超时时间掩盖。

5. 断言与调试技巧:少走弯路,快速定位问题

5.1 常用断言 API 一览

Playwright 的断言写法延续了 Jest 的风格,但背后加了一层自动重试的逻辑。常见的断言大概有这些:

await expect(locator).toBeVisible(); await expect(locator).toBeHidden(); await expect(locator).toHaveText('订单已提交'); await expect(locator).toContainText('成功'); await expect(locator).toHaveCount(3); await expect(page).toHaveURL('**/order/detail?id=123'); await expect(page).toHaveTitle('标题');

这里有一个比较容易忽略的细节:toHaveText是全量匹配,toContainText是包含匹配。比如一个元素的文本是“xxx 你好 xxx”,toHaveText('你好')会失败,而toContainText('你好')会通过。很多刚上手的朋友在这里吃过亏,特意提醒一下。

5.2 页面快照、Trace 与录屏调试

用例失败的时候,第一反应当然是想知道当时页面上发生了什么。控制台会把失败信息打印出来,但这远远不够。我喜欢用追踪查看器来分析,它几乎相当于把测试过程“录”了下来,包含页面截图、网络请求、DOM 快照和浏览器控制台日志。

要让追踪功能生效,只需要在命令行加一个参数:

npx playwright test --trace on

跑完用例后,会在test-results目录下生成.zip文件,再执行:

npx playwright show-trace

就能在浏览器里打开一个交互式的追踪报告。你可以逐步回放每一个操作,看到点击之前的元素状态、点击之后触发的网络请求、以及浏览器报错。定位一个偶发问题,比靠日志盲猜快太多了。

另外,如果你遇到“某些用例本地过,一到 CI 就挂”的经典问题,我强烈建议在 CI 失败的时候强制保留 trace。配置方式:

use: { trace: 'retain-on-failure', }

这样只有失败的用例会留下完整现场,既不浪费普通用例的存储,关键时候又有据可查。

5.3 利用 Inspector 和 Playwright 的代码生成器

还有一个新手友好度极高的功能:代码生成器(Codegen)。它可以把你在页面上的手动操作自动转成脚本。启动方式:

npx playwright codegen

运行后浏览器会打开一个窗口,你在里面点按钮、填表单、跳页面,代码面板会同步生成对应脚本。刚开始玩 Playwright 的时候,我会先把业务主流程用 codegen 过一遍,然后再手动整理选择器和步骤,效率提升非常明显。

不过 codegen 生成的脚本只是一个起点,它会生成很多绝对化的路径选择器,我的习惯是生成之后再人工改成语义化的 getByRole / getByText,并在合适的位置加上断言。

6. 测试报告与 CI/CD 集成:让自动化测试真正发挥价值

6.1 HTML 报告与 Allure 对比

测试报告是一个大家容易忽略、但实际使用体验影响非常大的部分。默认跑完npx playwright test之后,控制台输出的是一个简洁的用例列表,但如果想要更直观的统计信息、失败截图、console 日志,建议打开 HTML Report:

npx playwright show-report

Playwright 自带的 reporter 界面比较现代,按功能模块(suite、test、hooks 等)分组展示,点开失败用例能看到错误栈、附件(截图、trace 等)。大部分团队用它已经够了。

如果你的团队对报告有更高的展示要求,比如想按需求模块分类,或者要生成更精美的 PDF 报告,也可以引入 Allure。Playwright 有官方的 allure-playwright 插件,安装后配置 reporter 为allure-playwright,跑完用例再用命令行生成 Allure 报告。不过说实话,我从实际使用体验出发,先默认的 HTML 报告完全够用,等团队有特殊需求再上 Allure 也不迟。

6.2 GitHub Actions 集成示例

自动化测试如果不能跑在持续集成环境里,价值会大打折扣。这里用 GitHub Actions 举个例子。在项目根目录建.github/workflows/playwright.yml:

name: Playwright Tests on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: timeout-minutes: 30 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: '18' - run: npm ci - run: npx playwright install --with-deps - run: npx playwright test - uses: actions/upload-artifact@v3 if: always() with: name: playwright-report path: playwright-report/ retention-days: 7

重点说两个容易被忽略的细节:

  • --with-deps参数会同时安装浏览器运行所需的系统依赖库,在 Linux 环境里这步基本是必须的,否则浏览器可能起不来。
  • if: always()意思是即使用例跑挂了,也要把报告上传,否则你只能在日志里看到失败信息,拿不到现场 trace 和截图。

实际跑起来之后,PR 的每一次变更都会自动触发全量用例,配合 trace 和报告上传,相当于整个团队多了一个“不吃不喝的测试员”。

6.3 如何控制测试执行时间

自动化测试最容易被诟病的就是耗时。当用例从几十条涨到几百条时,执行时间可能从几分钟涨到几十分钟。我的处理思路是这样的:

  • 项目分目录:按业务模块建多个 spec 文件,CI 里可以并行跑多个任务。
  • 只跑关键冒烟集:在 PR 阶段可以只跑一个冒烟标记的用例集,全量回归放到合并前或者 nightly 定时任务里。
  • 合理使用 test.skip 和 test.fixme:暂时阻塞的功能直接标注跳过,别让它拖垮整体流程。

我个人最喜欢的是 Playwright 的标签机制,比如把用例标记为@smoke:

test.describe('核心交易流程', () => { test('下单并支付 @smoke', async ({ page }) => { // ... }); });

然后在命令行指定只跑冒烟用例:

npx playwright test --grep "@smoke"

这样既保证了主流程的快速反馈,又不需要把全量用例分布拆得乱七八糟。

7. 进阶实战:登录态复用、多页面交互与移动端模拟

7.1 登录态的保存与复用

做 B 端业务的自动化测试,登录状态几乎是每个用例都需要的前置条件。每次用例都走一遍登录流程,又慢又容易受到验证码、风控策略干扰。Playwright 支持把登录后的上下文状态保存下来,之后用例直接复用:

// 第一次登录并保存状态 const browser = await chromium.launch(); const context = await browser.newContext(); const page = await context.newPage(); // 登录操作... await context.storageState({ path: 'state.json' }); await browser.close();

后续用例就可以这样加载状态,跳过登录流程:

test.use({ storageState: 'state.json', });

这个方案在真实项目里效果立竿见影,登录耗时直接归零,而且能把登录模块的定制逻辑和普通业务用例解耦。

7.2 多页面交互与标签页切换

在实际业务里,很多操作是点开新标签页完成的,比如点击一个站外链接或者在新标签里打开详情页。Playwright 对这类场景的处理也比较直接:等待一个新的页面出现然后切换过去。

const [newPage] = await Promise.all([ context.waitForEvent('page'), page.click('a[target="_blank"]') ]); await newPage.waitForLoadState(); console.log(await newPage.title());

这里的手法是一个很典型的“事件监听 + 触发操作”组合:先注册等待事件,再执行点击,然后取出新页面对象。千万别先点击再去找新页面,那样存在竞态条件,容易偶发失败。

7.3 移动端视口与设备模拟

做移动端 H5 页面适配验证的时候,不需要非得开一堆真机。Playwright 可以通过修改上下文参数来模拟各类设备:

const device = devices['iPhone 13']; const context = await browser.newContext({ ...device, });

内置的设备描述符包含了userAgent、viewport、scaleFactor 等参数,你在页面上看到的渲染结果几乎和真机相差不大。对响应式布局的校验,这个功能很实用,我经常用它来快速横向验证不同尺寸下的样式有没有错位。

8. 避坑清单:那些实测中踩过、也帮你绕开的坑

这一节算是我个人最想写的内容。很多 Playwright 的教程只会教你 API 怎么调用,但实际落地过程中的细节问题,往往是文档里找不到的。

坑一:浏览器上下文不释放,导致内存占用快速爬升。虽然框架会帮我们清理,但是如果你在测试里手动创建了很多 context(比如反复调用browser.newContext()),用完记得await context.close(),否则跑大批量用例时内存会被吃满。这个问题在 Windows 环境上尤为明显。

坑二:网络请求断言太依赖响应体。有些接口会跨域访问,而 Playwright 的page.on('response')事件能看到所有网络活动,包括跨域请求。但别在事件回调里直接做同步断言,因为事件触发时机和页面交互进度不一定对齐。建议用waitForResponse的方式,把异步流程控制住。

坑三:定位器的“严格模式”。如果页面中有多个元素匹配了同一个定位器,Playwright 默认会报错,提示你定位器不唯一,而不是随便帮你选一个。刚开始可能会觉得烦,但这是为了减少误操作。遇到这种问题,正确姿势是进一步缩小范围,比如page.locator('.list').getByText('确认')。如果页面结构没法绕开,也可以用.first()或.nth(0),但我还是建议尽量保证定位的唯一性。

坑四:XPath 和文本定位里的反斜杠问题。某些特殊字符在 XPath 中需要转义,比如单引号、反斜杠;中文文本定位通常没问题,但如果带特殊符号建议使用正则表达式。排查的时候多留意报错信息里是“strict mode violation”还是“element not found”,两个问题解法完全不一样。

坑五:不要在测试脚本里依赖固定的等待时间。这一点前面反复说过,但还是要提一次。我在内部评审时看到最多的问题就是有人写着await page.waitForTimeout(3000)试图“确保”某个异步操作完成。除非是验证“网络超时”这类故意场景,否则这几乎都会导致用例在不稳定环境上波动。用expect的自动重试来代替无脑等待,是 Playwright 实践中最值得建立的习惯。

9. 一些个人体会

我之前花了不少时间维护老框架脚本,大部分精力其实都用在了“让脚本稳定”上,而不是真正“把业务测透”。转到 Playwright 以后,最大的感受是思路彻底被转换了:框架已经把稳定性、可诊断性这些工程问题解决了一大半,剩下只需要专注于描述业务场景和验证逻辑。

如果项目里正准备端到端测试选型,或者正在被老框架的稳定性折磨,我真心建议认真试一下 Playwright。把时间花在写业务用例上,而不是和“等待、网络、竞态”这些基础设施问题反复纠缠,这个投入的性价比是很高的。

最后再说一个小技巧:如果你的项目里有大量重复的业务操作步骤,比如新建订单、添加联系人、上传文件等,可以考虑把操作封装成自定义的 fixture 或者 Page Object 类,而不是每个用例把步骤重写一遍。封装以后,用例本身会变得非常薄,一眼就能看出这条用例到底在验证什么逻辑,这才是自动化测试长期可持续发展的关键。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进