改完代码怕线上崩:用 Playwright 给前端跑端到端测试
有次我改了个按钮的 class 名,自测点了一下没问题就发了。结果线上登录流程直接挂——因为有个地方用类名选中了那个按钮。单元测试覆盖不到这种"页面到底还能不能跑通"的事。后来给项目加了 Playwright 端到端测试,提交代码自动跑一遍主流程,这类低级事故再没出过。这篇讲怎么从零搭起来。

一、先装起来
一条命令搞定初始化:
npm init playwright@latest
它会问你几个问题(TypeScript 还是 JS、测试目录放哪、要不要加 GitHub Actions 工作流),选完自动生成:
playwright.config.ts # 配置:浏览器、baseURL、报告
tests/ # 你的用例
tests-examples/ # 官方示例,看完可以删
package.json
跑一下自带的示例,它会自动下载浏览器:
npx playwright test
二、写第一条用例
语法很直白,就是"打开页面 → 操作 → 断言结果":
import { test, expect } from '@playwright/test'
test('用账号密码登录并跳到首页', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('账号').fill('testuser')
await page.getByLabel('密码').fill('123456')
await page.getByRole('button', { name: '登录' }).click()
await expect(page).toHaveURL(/dashboard/)
await expect(page.getByText('欢迎回来')).toBeVisible()
})
注意 goto('/login') 用的是相对路径,域名在配置里统一给:
// playwright.config.ts
export default defineConfig({
use: {
baseURL: 'http://localhost:3000',
},
})
这样测试环境、预发环境换个 baseURL 就能跑,不用改用例。
三、定位元素别再用 xpath
这是最能决定"这套测试能活多久"的地方。同样选一个按钮,写法差别巨大:
| 写法 | 说明 | 稳定度 |
|---|---|---|
locator('//div[3]/button') |
xpath 按结构数层数 | 极差,DOM 一变就废 |
locator('#login-btn') |
靠 id / class | 差,改个样式就崩 |
getByRole('button', { name: '登录' }) |
按可访问性角色+名字 | 好,跟着用户看到的走 |
getByLabel('账号') |
按 label 关联的输入框 | 好 |
getByTestId('login-submit') |
按约定的测试标记 | 好,但要团队先约定 |
优先用 getByRole。它选的是"用户眼里的那个按钮",你把 div 换成 button、把 class 改个名,测试照样过。顺带一个好处:逼着你把语义化的可访问性属性补上,对无障碍也友好。
还有个特别省心的设计:Playwright 自带自动等待。expect(...).toBeVisible() 会一直重试直到超时(默认 5 秒),异步渲染的页面也不用你操心。所以千万别写 waitForTimeout(3000)——那只会让测试又慢又脆。
四、登录态复用:别每条用例都登一遍
大部分页面都要登录,每条用例都跑一遍登录流程,几十条用例能多花好几分钟。正确做法是先跑一个"登录专用"的用例,把登录状态存成文件,其余用例直接复用。
// tests/auth.setup.ts
import { test as setup } from '@playwright/test'
setup('先登录一次', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('账号').fill('testuser')
await page.getByLabel('密码').fill('123456')
await page.getByRole('button', { name: '登录' }).click()
await page.waitForURL(/dashboard/)
await page.context().storageState({ path: 'auth.json' }) // 把 cookie/localStorage 存下来
})
配置里声明依赖关系:
export default defineConfig({
testDir: './tests',
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: 'auth.json' },
dependencies: ['setup'], // 跑我之前先跑 setup
},
],
})
auth.json 记得加进 .gitignore,里面是真实的登录凭据。
五、挂进流水线自动跑
本地能跑之后,真正的价值在"每次提交自动跑"。一份能用的配置长这样:
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI, // 防止有人把 .only 提交上来
retries: process.env.CI ? 2 : 0, // CI 上允许重试,抗抖动
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'retain-on-failure', // 只在失败时录 trace
screenshot: 'only-on-failure',
},
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: { // 自动起前端服务
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
GitHub Actions 里加几个步骤:
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps chromium # Linux 上需要 sudo
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 7
用华为云 CodeArts Pipeline 也一个道理,在流水线里加一条执行 shell 的步骤跑 npx playwright test,再把 playwright-report 目录归档成构建产物就行。
用例挂了要排查,看报告:
npx playwright show-report
失败的那条能直接打开 Trace Viewer——每一步操作的截图、DOM 快照、网络请求、控制台日志全在里面,基本不用在本地复现就能定位。
几个容易卡的地方
- 别用
waitForTimeout。要等某个元素出现就用expect(locator).toBeVisible(),要等接口返回就用page.waitForResponse()。写死 sleep 是端到端测试变脆的头号原因。 - 测试数据必须独立。每条用例自己造数据、自己清理。并行跑的时候如果两条用例共用一个账号改同一份数据,会随机失败,而且极难复现。
- CI 上建议
workers: 1。本地多 worker 并行很快,但 CI 机器配置一般,并行容易超时抖动。稳比快重要。 - trace 别开
'on'。全量录制体积大得吓人,retain-on-failure或者on-first-retry性价比最高。 - 端到端测试别贪多。按测试金字塔,它应该是最上面最少的那一层。守住登录、下单、支付这种主流程,写十几条就够;全靠它兜底,跑一次半小时,团队很快就会把它屏蔽掉——那等于没写。
- 别对着生产环境跑。造出来的订单、发出去的通知都是真数据。单独准备一套测试环境,每次跑完重置。
--with-deps在 Linux 上要 root。装系统依赖需要权限,容器里一般没问题,自己的虚拟机记得加sudo。
总结
端到端测试解决的是一个很朴素的问题:代码改动之后,用户真正会走的那几条路还能不能通。Playwright 上手门槛不高——npm init playwright 一条命令,写用例几乎就是"点哪里、填什么、期待看到什么",选对 getByRole 这类定位方式之后也不容易脆。真正要花心思的是把它放进流水线里持续跑,以及控制住用例数量。
- 点赞
- 收藏
- 关注作者
评论(0)