简介:这份文档面向希望快速上手现代端到端自动化测试的开发者与测试人员,系统讲解Playwright从环境搭建到脚本实战的完整路径。内容涵盖Playwright跨浏览器特性与核心概念,详细说明Python环境下PyCharm与pytest-playwright的安装配置流程,并通过百度首页输入、点击、Web断言等实战案例演示脚本编写,同时涉及命令行参数配置、录制用例、base_url使用以及Click、hover、下拉框操作等常用方法实践。资源包为单一docx文档,压缩包约17.16MB,目录按章节组织,从介绍、环境安装到脚本实战与方法实践层层递进,便于按模块查阅学习。目前已有377人学习下载,适合需要掌握跨浏览器测试、对比传统框架并积累实战经验的读者参考。
1. 从一份文档到一套可跑的 Playwright 自动化测试:先搞清楚它到底解决什么问题
很多人第一次接触 Playwright 自动化测试,是因为拿到了一份需求文档或者测试用例文档,里面列了几十条“点击登录按钮,验证跳转首页”这样的步骤,然后被要求“把这些自动化起来”。如果你直接打开编辑器开始写page.click(),大概率会在第三天陷入维护地狱:选择器到处失效、等待时间全靠sleep、失败截图找不到、CI 上跑得比本地慢三倍。Playwright 自动化测试真正要解决的不是“把手工步骤翻译成代码”,而是把一份静态文档变成一套可重复执行、失败可定位、能进流水线的测试资产。它适合两类人:一类是手工测试转自动化、手里有大量用例文档但不知道怎么落地的工程师;另一类是做 Web 前端或全栈、想给自己的项目加一层端到端回归防线但不想折腾 Selenium 那套环境的人。这一章先把“文档到代码”的映射关系讲清楚,后面几章再一步步把环境、脚本、参数和坑填上。
2. 把文档里的测试用例翻译成 Playwright 脚本:从一条用例开始
2.1 先别急着写代码,把文档拆成“动作 + 断言 + 数据”三列
一份典型的测试用例文档,一行通常长这样:“输入正确的用户名和密码,点击登录,验证进入首页,且右上角显示用户名”。直接翻译成代码很容易写成流水账,正确的做法是先拆成三列:动作(输入、点击)、断言(URL 变化、元素可见)、数据(用户名、密码)。我一般会在文档旁边开一个表格,把每条用例拆成这三类,拆不出来的说明这条用例本身写得不够可执行,需要先回去补文档。这一步不做,后面写出来的脚本一定是“能跑但不知道在测什么”。
拆完之后,一条用例对应一个test()块,动作和断言交替出现,数据用参数化传入。Playwright 的test.step()可以把每个动作包起来,失败时报告里能直接看到是哪一步挂了,而不是只告诉你“第 37 行报错”。
2.2 最小可运行脚本:打开页面、定位元素、断言
下面这段代码是一个最小可运行的 Playwright 测试,假设你已经用npm init playwright@latest初始化过项目。它做的事情就是文档里最常见的那条“登录后验证首页”:
// tests/login.spec.js const { test, expect } = require('@playwright/test'); test('用正确账号登录后进入首页', async ({ page }) => { // 动作:打开登录页 await page.goto('https://example.com/login'); // 动作:填入用户名和密码,getByLabel 比 CSS 选择器更贴近文档描述 await page.getByLabel('用户名').fill('testuser'); await page.getByLabel('密码').fill('testpass123'); // 动作:点击登录按钮 await page.getByRole('button', { name: '登录' }).click(); // 断言:URL 变成首页 await expect(page).toHaveURL(/.*\/home/); // 断言:右上角显示用户名 await expect(page.getByText('testuser')).toBeVisible(); });逻辑说明:page.goto负责导航,getByLabel和getByRole是 Playwright 推荐的用户可见定位方式,比page.locator('.btn-primary')稳定得多,因为 class 名会随前端重构变化,而 label 和 role 是给用户看的。expect(page).toHaveURL是自动等待的断言,Playwright 会在超时前反复检查,不需要你手写waitForTimeout。
参数说明:test和expect从@playwright/test引入,不要从playwright引入,后者是库模式,没有测试运行器和断言。fill会先清空再输入,如果你需要模拟逐字输入用pressSequentially。toHaveURL接受字符串或正则,正则更抗路径参数变化。
2.3 把多条用例组织成测试套件:describe、beforeEach 和参数化
文档里通常一个模块下有十几条用例,比如“登录模块”下有正确登录、错误密码、空用户名、记住我勾选等。这时候用test.describe分组,用beforeEach做公共前置(比如每条用例前都打开登录页),用数组参数化跑多组数据:
const { test, expect } = require('@playwright/test'); const loginCases = [ { name: '正确账号', user: 'testuser', pass: 'testpass123', expectSuccess: true }, { name: '错误密码', user: 'testuser', pass: 'wrongpass', expectSuccess: false }, { name: '空用户名', user: '', pass: 'testpass123', expectSuccess: false }, ]; test.describe('登录模块', () => { beforeEach(async ({ page }) => { await page.goto('https://example.com/login'); }); for (const c of loginCases) { test(c.name, async ({ page }) => { await page.getByLabel('用户名').fill(c.user); await page.getByLabel('密码').fill(c.pass); await page.getByRole('button', { name: '登录' }).click(); if (c.expectSuccess) { await expect(page).toHaveURL(/.*\/home/); } else { await expect(page.getByText('用户名或密码错误')).toBeVisible(); } }); } });逻辑说明:beforeEach里的page.goto对每条用例都会执行一次,保证用例之间互不污染。参数化用for...of循环生成test,比写四遍几乎一样的代码好维护。expectSuccess这种布尔开关在用例少的时候够用,用例多了建议拆成两个 describe,避免一个 test 里塞太多分支。
参数说明:beforeEach的page是 Playwright 自动注入的 fixture,每个 test 拿到的是独立的 page 实例,不要试图在多个 test 之间共享 page,否则会出现状态串扰。test.describe可以嵌套,但嵌套层级建议不超过两层,否则报告可读性下降。
3. 选择器、等待和断言:Playwright 自动化测试里最容易翻车的三件事
3.1 选择器优先级:为什么 getByRole 比 CSS 更值得用
Playwright 官方推荐的选择器优先级是:getByRole>getByLabel/getByPlaceholder/getByText>getByTestId> CSS / XPath。这个顺序不是随便排的,它对应的是“用户和辅助技术看到什么”。getByRole('button', { name: '登录' })找的是无障碍树里角色为 button、可访问名称为“登录”的元素,前端把 class 从btn-primary改成btn-submit不影响它,把按钮换成<a role="button">也不影响。CSS 选择器则相反,任何一次样式重构都可能让它失效。
但getByRole不是万能的。如果页面里有两个“登录”按钮(比如顶部导航和表单各一个),就需要加exact: true或者用filter缩小范围。我一般会在开发阶段让前端在关键元素上加>// 点击查询后等待接口返回再断言表格 const responsePromise = page.waitForResponse( (res) => res.url().includes('/api/list') && res.status() === 200 ); await page.getByRole('button', { name: '查询' }).click(); await responsePromise; await expect(page.getByRole('table')).toContainText('张三');
逻辑说明:waitForResponse必须在触发请求之前调用,否则可能错过响应。这里先创建 promise,再点击,再 await,顺序不能反。
参数说明:回调里的res是 Response 对象,可以用url()、status()、json()过滤。如果接口是 GraphQL 或者 URL 带随机参数,建议用res.request().postData()判断请求体。
第二类等待动画,优先用expect(locator).toBeVisible()而不是waitForTimeout。第三类等待消失用expect(locator).toBeHidden()。waitForTimeout只在调试时用,不要提交到代码库,否则 CI 上会变成随机失败的源头。
3.3 断言粒度:一条用例到底该有多少个 expect
文档里一条用例可能写了五六个验证点,但翻译成代码时不要在一个 test 里堆二十个expect。我的经验是:一条 test 只验证一个核心行为,辅助验证点用test.step包起来,但断言数量控制在 3 到 5 个。超过这个数,失败时你很难判断到底是哪个环节出了问题。
另外,expect的软断言(expect.soft)适合用在“即使这条挂了也想继续跑后面验证”的场景,比如检查页面多个字段的文案。但软断言不能滥用,核心流程的断言必须用硬断言,否则测试报告会给你一种“都通过了”的错觉。
4. 配置、并行和 CI:让 Playwright 自动化测试真正跑起来
4.1 playwright.config.js 里必须改的四个参数
初始化项目会生成一个默认的playwright.config.js,默认配置能跑,但离“能用”还差四个参数:
// playwright.config.js const { defineConfig, devices } = require('@playwright/test'); module.exports = defineConfig({ testDir: './tests', timeout: 30 * 1000, // 单条用例超时,默认 30 秒,接口慢的调到 60 秒 retries: process.env.CI ? 2 : 0, // CI 上失败重试两次,本地不重试 workers: process.env.CI ? 2 : undefined, // CI 上限制并行数,避免资源争抢 reporter: [['html'], ['list']], // 本地看 html 报告,CI 看 list 日志 use: { baseURL: 'https://example.com', // 所有 page.goto 可以写相对路径 trace: 'on-first-retry', // 第一次重试时录 trace,方便排查 screenshot: 'only-on-failure', // 失败截图 video: 'retain-on-failure', // 失败保留视频 }, projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, ], });逻辑说明:baseURL设了之后,脚本里page.goto('/login')会自动拼成完整地址,切换测试环境只需要改这一个地方。trace是 Playwright 的“后悔药”,失败重试时录下完整操作轨迹,用npx playwright show-trace打开,能看到每一步的 DOM 快照和网络请求。
参数说明:retries在 CI 上设 2 是常见做法,但要注意重试会掩盖偶发问题,如果某条用例每次都要重试才过,应该去查根因而不是调大重试次数。workers在 CI 上设 2 到 4 取决于机器核数,设太高会导致浏览器启动失败。
4.2 并行执行和测试隔离:为什么你的用例不能共享登录状态
Playwright 默认按文件并行,同一个文件内的 test 串行。如果你在beforeAll里登录一次然后所有 test 共用,一旦某个 test 改了用户状态,后面的 test 就会连锁失败。正确做法是每条 test 独立登录,或者用storageState复用登录态但只读不写。
// 全局登录一次,保存 storageState // global-setup.js const { chromium } = require('@playwright/test'); module.exports = async () => { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto('https://example.com/login'); await page.getByLabel('用户名').fill('testuser'); await page.getByLabel('密码').fill('testpass123'); await page.getByRole('button', { name: '登录' }).click(); await page.waitForURL(/.*\/home/); await page.context().storageState({ path: 'auth.json' }); await browser.close(); };然后在 config 里use: { storageState: 'auth.json' }。这样每条 test 启动时已经是登录态,省去重复登录的时间,同时因为 storageState 是只读加载,不会互相污染。注意:如果测试涉及修改密码、退出登录,这些用例要单独用新的 context,不能复用 auth.json。
4.3 接入 CI:一条命令跑完并产出可读报告
CI 上跑 Playwright 最常见的问题是浏览器没装、依赖缺失、报告拿不到。以 GitHub Actions 为例,核心步骤就三步:装依赖、装浏览器、跑测试。
# .github/workflows/playwright.yml name: Playwright Tests on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx playwright install --with-deps chromium - run: npx playwright test - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report path: playwright-report/逻辑说明:--with-deps会安装浏览器运行需要的系统库,不加这个在 Ubuntu 上大概率启动失败。if: always()保证测试失败时报告也能上传,否则你只能看到“失败”两个字。
参数说明:npx playwright install只装 chromium 可以加快 CI 速度,如果 projects 里配了 firefox 和 webkit,这里要对应加上。upload-artifact的path指向 config 里 reporter 生成的目录,默认是playwright-report。
5. 避坑与排查:Playwright 自动化测试里最常见的五类翻车
5.1 元素定位到了但点击没反应
现象:click()不报错,但页面没跳转。原因通常是元素被遮挡(弹窗、固定头部)或者点击的是不可见的副本。解决:先用await locator.isVisible()和isEnabled()确认状态,再用click({ force: true })强制点击,但 force 是最后手段,优先检查 DOM 结构。如果是弹窗遮挡,先关弹窗再点。
5.2 本地能过 CI 挂,报 timeout
现象:本地跑得好好的,CI 上大量超时。原因一般是 CI 机器性能差、网络慢、并行数太高。解决:把workers降到 2,timeout调到 60 秒,retries设 2。如果还挂,检查是不是baseURL指向了本地地址而 CI 访问不到。另外 CI 上首次启动浏览器会慢,可以在beforeAll里做一次预热。
5.3 断言文本时好时坏,怀疑是玄学
现象:toContainText('成功')偶尔失败,截图里明明有“成功”两个字。原因通常是文本被拆分到多个子元素,或者有不可见字符。解决:用toContainText而不是toHaveText,后者要求完全匹配。如果还不行,用locator.innerText()打印出来看实际内容,经常能发现多了空格或换行。
5.4 上传文件、下载文件、新窗口切换不会写
现象:文档里有“上传附件”“下载报表”“点击后新开标签页”的用例,不知道从哪下手。解决:上传用setInputFiles,下载用waitForEvent('download'),新窗口用context.waitForEvent('page')。这三个 API 在 Playwright 里都有专门支持,不要试图用点击文件选择框的方式绕过。
5.5 测试数据被污染,第二条用例必挂
现象:第一条用例注册了用户,第二条用例用同一个用户名注册就失败。原因是用例之间共享了后端数据。解决:每条用例用随机数据(时间戳或 uuid),或者用 API 在beforeEach里清理数据。Playwright 的requestfixture 可以直接调接口做数据准备,比走 UI 快得多。
6. 进阶技巧:用 trace 和 codegen 把调试时间砍掉一半
6.1 trace 查看器:失败现场的“黑匣子”
trace: 'on-first-retry'只是第一步,真正有用的是npx playwright show-trace test-results/xxx/trace.zip。打开后你能看到时间轴上的每一步操作、对应的 DOM 快照、网络请求、console 日志。我排查过一个“点击后页面白屏”的问题,trace 里显示点击后有一个 500 的接口请求,前端没做错误处理直接白了。如果没有 trace,你可能要花半天去复现。
trace 文件会占空间,CI 上建议只保留失败用例的 trace,config 里trace: 'retain-on-failure'也可以。本地调试时临时改成trace: 'on',跑完记得改回来。
6.2 codegen:从手工操作生成脚本草稿
npx playwright codegen https://example.com会打开一个浏览器和代码生成器,你在页面上的每一步操作都会实时生成 Playwright 代码。这个工具适合用来快速拿到选择器,但生成的代码不能直接当测试用,因为它不会帮你加断言、不会参数化、选择器也可能不够稳定。我的习惯是用 codegen 走一遍流程,把生成的选择器复制出来,然后自己重写成带断言的 test。
6.3 用 API 做数据准备,UI 只做验证
端到端测试最慢的环节是走 UI 做前置条件。比如测试“订单列表分页”,如果先用 UI 创建 20 个订单,光这一步就要几分钟。更好的做法是用requestfixture 直接调创建订单的接口,然后 UI 只打开列表页验证分页控件。这样一条用例从 3 分钟降到 10 秒。
test('订单列表分页显示正确', async ({ page, request }) => { // 用 API 快速造 20 条数据 for (let i = 0; i < 20; i++) { await request.post('/api/orders', { data: { product: `商品${i}`, amount: 100 + i }, }); } await page.goto('/orders'); await expect(page.getByRole('row')).toHaveCount(11); // 10 条数据 + 表头 await page.getByRole('button', { name: '下一页' }).click(); await expect(page.getByRole('row')).toHaveCount(11); });逻辑说明:request是 Playwright 内置的 API 请求 fixture,和page共享 cookie 和 baseURL,不需要额外装 axios。toHaveCount是自动等待的,不用手动 sleep。
参数说明:request.post的data会自动序列化为 JSON 并设置 Content-Type。如果接口需要 token,可以在 config 里配extraHTTPHeaders,或者用storageState里的 cookie 自动带上。
6.4 我踩过的一个坑:不要用page.waitForTimeout当万能药
刚用 Playwright 的时候,遇到任何“元素还没出来”就加waitForTimeout(3000),结果脚本里到处都是 3 秒等待,跑一遍要十几分钟,CI 上还经常因为机器慢而失败。后来强制自己把所有waitForTimeout删掉,换成expect自动等待和waitForResponse,同样的用例集从 12 分钟降到 4 分钟,失败率也降了。这个习惯我保持到现在:代码里出现waitForTimeout就要问自己,到底在等什么,能不能用条件等待替代。希望帮到你。
本文还有配套的精品资源,点击获取