用 Playwright 搭建一套稳定的前端回归测试:从 30% 误报到零维护成本|2026-11-12
前端项目写单元测试相对容易,但真正能拦住线上事故的往往是端到端测试。这篇文章记录我们团队把 Playwright 落地到实际项目的完整过程,包括那些官方文档不会告诉你的细节。
为什么放弃 Selenium
之前用 Selenium + ChromeDriver,主要痛点是:
- 驱动版本必须和浏览器严格对应,CI 上经常因为自动升级失败
- 没有自动等待机制,所有操作都要手写
WebDriverWait,代码里到处是sleep - 多标签页、文件下载、iframe 处理都要自己封装
Playwright 的自动等待(auto-waiting)解决了 70% 的稳定性问题:点击前自动等元素可交互,导航后自动等网络空闲。
项目结构
e2e/
├── playwright.config.ts
├── fixtures/
│ └── auth.ts # 登录态复用
├── pages/ # 页面对象
│ ├── login.page.ts
│ └── order.page.ts
├── specs/
│ ├── smoke.spec.ts
│ └── order.spec.ts
└── .auth/
└── user.json # 序列化的登录态
配置要点
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e/specs',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [['html', { open: 'never' }], ['list']],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:5173',
trace: 'on-first-retry', // 只在失败重试时录
video: 'retain-on-failure',
screenshot: 'only-on-failure',
locale: 'zh-CN',
timezoneId: 'Asia/Shanghai',
},
projects: [
{ name: 'setup', testMatch: /auth\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' },
dependencies: ['setup'],
},
],
})
几个关键决策:
retries 在 CI 上开 2 次。 不是为了让测试"看起来绿",而是因为真实环境的网络抖动确实存在,重试能把偶发失败过滤掉。但本地必须开 0,否则问题会被掩盖。
trace 只录制失败重试。 全量录制会让 CI 产物涨到几个 G,且拖慢执行。
登录态复用
每个测试都走一遍登录是最常见的性能杀手。正确做法是全局登录一次,序列化 storageState:
// e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test'
const authFile = '.auth/user.json'
setup('登录并保存状态', async ({ page }) => {
await page.goto('/login')
await page.getByRole('textbox', { name: '用户名' }).fill(process.env.E2E_USER!)
await page.getByRole('textbox', { name: '密码' }).fill(process.env.E2E_PASS!)
await page.getByRole('button', { name: '登录' }).click()
await expect(page.getByRole('heading', { name: '工作台' })).toBeVisible()
await page.context().storageState({ path: authFile })
})
这样 30 个用例能省下 2 分钟以上,而且避免了登录接口被风控限流。
注意:storageState 只保存 cookie 和 localStorage,sessionStorage 不会保存。如果有 token 存在 sessionStorage 里,需要在每个用例的 beforeEach 里手动注入。
选择器策略
这是决定维护成本的核心。优先级:
1. getByRole + 可访问名称(最稳)
2. getByLabel / getByPlaceholder
3. getByTestId
4. CSS / XPath(最后手段)
// 差:绑死 DOM 结构,改个样式就挂
await page.click('div.container > div:nth-child(3) > button.btn-primary')
// 好:绑语义
await page.getByRole('button', { name: '提交订单' }).click()
我们实际统计过:用 nth-child 的选择器平均 2 周就要修一次;用 role 的半年没动过。
对于确实找不到语义的复杂组件,加 data-testid,并约定 testid 一旦加上就不能随便改,改了就等同于改接口。
处理不稳定因素
1. 动画导致点击落空
// 全局关动画
await page.addInitScript(() => {
const style = document.createElement('style')
style.textContent = `*, *::before, *::after {
transition-duration: 0s !important;
animation-duration: 0s !important;
animation-delay: 0s !important;
}`
document.head.appendChild(style)
})
2. 接口慢导致的假失败
不要用 waitForTimeout,改成等业务信号:
// 差
await page.click('#load')
await page.waitForTimeout(3000)
// 好
await page.click('#load')
await expect(page.getByText('加载完成')).toBeVisible()
或者直接等接口:
const respPromise = page.waitForResponse(r => r.url().includes('/api/orders') && r.status() === 200)
await page.getByRole('button', { name: '刷新' }).click()
await respPromise
3. 弹窗遮挡
element is not clickable 报错有 90% 是 mask 层没消失。封装一个等待:
await page.locator('.el-loading-mask').waitFor({ state: 'detached', timeout: 10_000 })
CI 集成
- name: Install browsers
run: npx playwright install --with-deps chromium
- name: Run e2e
run: npx playwright test
env:
BASE_URL: http://localhost:8080
E2E_USER: ${{ secrets.E2E_USER }}
E2E_PASS: ${{ secrets.E2E_PASS }}
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
实施后的数据
接入 3 个月后的统计:
指标 | 数值
用例数 | 68
单次全量执行 | 4 分 12 秒
首次运行误报率 | 31%
当前误报率 | 2.4%
捕获的真实缺陷 | 23 个(其中 6 个是 P1)
误报率从 31% 降到 2.4%,主要靠三件事:把所有 waitForTimeout 换成业务信号等待、选择器全面换成 role/testid、失败时用 trace 定位而不是猜。
一点经验
E2E 测试的价值不在于覆盖率数字,而在于 能不能稳定地在 5 分钟内告诉你"这次改动有没有砸掉核心流程"。为了这个目标,宁可只写 50 个高价值用例,也不要写 300 个跑 40 分钟还天天红的用例集。
最后一个建议:把 trace 查看器(npx playwright show-trace)教给团队里所有人,包括产品。失败时打开 trace 能直接看到每一步的 DOM 快照和控制台,沟通成本会低很多。
- 点赞
- 收藏
- 关注作者
评论(0)