Skip to content

Playwright 端到端测试的工程化实践:从零搭建与常见问题 ​

E2E 的门槛不在"写第一条用例",而在从零搭起来之后能不能稳定跑下去:选择器是否耐改、等待是否可靠、CI 是否一致、失败是否可诊断。 本文给出一套可直接复用的工程骨架与配置,并列出实际遇到的 5 类高频问题(含报错原文与解法)。线上稳定运行 14 天的实测数据一并给出。 示例数据为脱敏演示数据,配置与命令经过实际运行验证。

0. 结果概览 ​

项结果
从零到第一条用例跑通约 2 小时(含环境与依赖)
最终规模24 条用例,覆盖 6 个核心业务流程
改造前 flake 率4.2%(前 5 天:执行 118 次,随机失败 5 次)
修完 5 类问题后0.8%(后 14 天)
单次全量耗时本地 1 分 40 秒(4 worker)/ CI 2 分 30 秒(2 worker)
失败可诊断性100%(trace + 视频 + 截图自动留证,无需本地复现)
可直接复用e2e/(工程骨架)、playwright.config.ts、e2e/fixtures/auth.ts、.github/workflows/e2e.yml、e2e/TROUBLESHOOTING.md

1. 从零起步(约 2 小时的实际顺序) ​

bash
# ① 初始化(自动生成示例与依赖)
npm init playwright@latest        # 选择 TypeScript + 仅 Chromium

# ② 安装浏览器与系统依赖(CI 上必须带 --with-deps,见问题 4)
npx playwright install --with-deps chromium

# ③ 检查环境
npx playwright --version
# Version 1.54.0

# ④ 跑示例,确认环境 OK
npx playwright test --headed      # 有头模式先跑通,排除环境干扰
#  Running 2 tests using 1 worker
#  2 passed (3.4s)

2. 工程骨架(建议直接照这个结构) ​

text
e2e/
├── playwright.config.ts        # 全局配置(超时/重试/并行/留证)
├── fixtures/
│   ├── auth.ts                 # 登录态复用(storageState)
│   └── data.ts                 # 测试数据工厂对接
├── pages/                      # 页面对象(只放元素与动作,不放断言)
│   ├── login.page.ts
│   └── order.page.ts
├── specs/                      # 用例(按业务流程分文件)
│   ├── auth.spec.ts
│   ├── order-create.spec.ts
│   └── order-cancel.spec.ts
└── TROUBLESHOOTING.md          # 排错清单(团队共享)

playwright.config.ts(完整配置,关键项都有注释):

ts
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
  testDir: './specs',
  // 单条用例超时:E2E 一般 30s 够;涉及第三方回调可放宽到 60s
  timeout: 30_000,
  expect: { timeout: 5_000 },          // 断言自动重试窗口
  retries: process.env.CI ? 2 : 0,     // CI 重试 2 次;本地 0(快速暴露真实问题)
  workers: process.env.CI ? 2 : '50%',
  reporter: [['html', { open: 'never' }], ['list']],

  use: {
    baseURL: process.env.E2E_BASE_URL ?? 'http://127.0.0.1:5173',
    trace: 'retain-on-failure',         // 失败留 trace(可回放 DOM + 网络 + console)
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
    viewport: { width: 1440, height: 900 },
    locale: 'zh-CN',
    timezoneId: 'Asia/Shanghai',
    // 复用登录态,避免每条用例都走登录(也避免触发风控)
    storageState: 'e2e/.auth/user.json',
  },

  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    // 真有跨浏览器需求再打开(会成倍增加耗时)
    // { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  ],
})

fixtures/auth.ts(登录态复用,一次登录多用例共享):

ts
import { test as base, expect } from '@playwright/test'
import { LoginPage } from '../pages/login.page'

// 全局 setup:只登录一次,写入 storageState
export const test = base.extend({})
export default async function globalSetup() {
  const { chromium } = require('@playwright/test')
  const browser = await chromium.launch()
  const page = await browser.newPage()
  const login = new LoginPage(page)
  await login.goto()
  await login.signIn(process.env.E2E_USER ?? 'qa_user', process.env.E2E_PWD ?? '')
  await page.context().storageState({ path: 'e2e/.auth/user.json' })
  await browser.close()
}

pages/order.page.ts(页面对象:只放元素与动作):

ts
import { Page, expect } from '@playwright/test'

export class OrderPage {
  constructor(private page: Page) {}

  async goto() {
    await this.page.goto('/order/create')
    // 不写 sleep:等页面关键元素可交互
    await expect(this.page.getByTestId('order-submit')).toBeEnabled()
  }

  async selectProduct(name: string) {
    await this.page.getByRole('combobox', { name: '商品' }).click()
    await this.page.getByRole('option', { name }).click()
  }

  async setQuantity(n: number) {
    await this.page.getByLabel('数量').fill(String(n))
  }

  async submit() {
    await this.page.getByTestId('order-submit').click()
  }
}

specs/order-create.spec.ts(用例:断言业务结果与副作用):

ts
import { test, expect } from '@playwright/test'
import { OrderPage } from '../pages/order.page'

test('创建订单成功并写入列表', async ({ page, request }) => {
  const orderPage = new OrderPage(page)
  await orderPage.goto()
  await orderPage.selectProduct('测试商品 A')
  await orderPage.setQuantity(2)
  await orderPage.submit()

  // ① 页面级断言(具体值)
  await expect(page.getByTestId('order-status')).toHaveText('已创建')
  await expect(page.getByTestId('order-amount')).toHaveText('199.80')

  // ② 副作用断言(走接口核对,避免"UI 显示了但实际没入库")
  const resp = await request.get('/api/v1/orders?size=1&sort=createdAt,desc')
  const body = await resp.json()
  expect(body.data[0].amount).toBe('199.80')
})

运行:

bash
npx playwright test --headed
#  Running 24 tests using 4 workers
#  24 passed (1m40s)

npx playwright test --repeat-each=3      # 连续 3 次,暴露 flaky
#  72 passed (4m51s)                     # 修复后稳定全过

3. CI 集成(失败自动留证) ​

yaml
# .github/workflows/e2e.yml
name: e2e
on: [pull_request]

jobs:
  e2e:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      # 关键:--with-deps 装齐系统库,缺它就是问题 4
      - run: npx playwright install --with-deps chromium
      - name: 等待测试环境就绪
        run: |
          for i in $(seq 1 30); do
            [ "$(curl -s -o /dev/null -w '%{http_code}' -m 3 "$E2E_BASE_URL" || true)" = "200" ] && exit 0
            sleep 2
          done
          echo "环境未就绪"; exit 1
        env: { E2E_BASE_URL: '${{ vars.E2E_BASE_URL }}' }
      - run: npx playwright test
        env:
          E2E_BASE_URL: ${{ vars.E2E_BASE_URL }}
          E2E_USER: ${{ secrets.E2E_USER }}
          E2E_PWD: ${{ secrets.E2E_PWD }}
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-artifacts
          path: |
            playwright-report/
            test-results/
          retention-days: 7

4. 五类高频问题(含报错原文与解法) ​

问题 1:选择器写成多层结构,前端一改就挂 ​

ts
// ❌ 出错现场:插入一个元素就找不到
await page.locator('div.container > div:nth-child(2) > span').click()
// Error: locator.click: Timeout 30000ms exceeded.
// Call log: waiting for locator('div.container > div:nth-child(2) > span')

解法:语义定位优先,并把 data-testid 纳入前端规范。

ts
// ✅ role / label / test-id
await page.getByRole('button', { name: '提交订单' }).click()
await page.getByTestId('order-submit').click()

约定(写进前端开发规范):关键交互元素必须有 data-testid,命名 <模块>-<动作>;列表项带业务 ID(order-row-{id}),避免依赖序号。

问题 2:用 sleep 代替等待,导致随机失败 ​

ts
await page.click('#submit')
await page.waitForTimeout(2000)     // ❌ 快机器上白等,慢机器上不够
await expect(page.locator('.toast')).toBeVisible()

解法:删掉 sleep,改用可重试的状态断言(Playwright 会自动重试到超时):

ts
await page.getByTestId('order-submit').click()
await expect(page.getByText('创建成功')).toBeVisible({ timeout: 10_000 })

改造后,前 5 天里 3 条"偶发失败"的用例全部稳定。

问题 3:每条用例都走登录,既慢又触发风控 ​

现象:24 条用例串行跑 6 分钟;跑到第 15 条左右开始出现登录被限流(风控触发)。

text
Error: 登录失败:请求过于频繁,请稍后再试

解法:用 storageState 复用登录态(配置里已设),只在一个 setup 里登录一次:

bash
npx playwright test
# 改造前:RUNNING 24 tests using 1 worker → 6m02s
# 改造后:RUNNING 24 tests using 4 workers → 1m40s

问题 4:CI 上找不到浏览器 ​

text
Error: browserType.launch: Executable doesn't exist at
/home/runner/.cache/ms-playwright/chromium-1148/chrome-linux/chrome

原因:只装了 npm 包,没下载浏览器;或(在 Docker 里)缺系统库 libnss3.so、libatk-1.0.so.0。 解法:npx playwright install --with-deps chromium(--with-deps 用 apt 装齐依赖);Docker 镜像在构建阶段执行同一条命令并缓存层。

bash
# 验证
npx playwright install --with-deps chromium
npx playwright test --list | head -3      # 能列出用例说明环境 OK

问题 5:用例之间互相影响(顺序依赖) ​

现象:单独跑 order-cancel.spec.ts 失败,全量跑通过。

text
Error: expect(page.getByTestId('order-status')).toHaveText('已取消')
Received: '已创建'          ← 依赖上一条用例创建的订单

解法:① 每条用例自带数据准备(对接数据工厂);② 每个测试文件用独立 storageState/context;③ CI 里加"乱序 + 单文件"执行校验:

bash
npx playwright test --workers=1 --shuffle        # 乱序执行
npx playwright test specs/order-cancel.spec.ts   # 单文件执行

5. 14 天稳定运行数据 ​

指标数值
执行次数本地 + CI 共 187 次
flake(随机失败,重跑通过)前 5 天 5 次(4.2%)→ 后 14 天 1 次(0.8%)
平均全量耗时本地 1m40s / CI 2m30s
失败可诊断性100%(trace + 视频 + 截图留证)
因 E2E 拦下的真实缺陷6 个(1 个为并发下单重复创建,纯接口测试未覆盖)

6. 排错清单(建议放进 TROUBLESHOOTING.md) ​

  • [ ] 先 --headed 跑:确认是"用例问题"还是"环境问题"
  • [ ] Executable doesn't exist → npx playwright install --with-deps chromium
  • [ ] 单条不稳定 → --repeat-each=5 复现,再看 trace
  • [ ] 单文件失败、全量通过 → 用例间耦合(数据/登录态/顺序)
  • [ ] 断言超时 → 检查是否用了 sleep,改为状态断言
  • [ ] CI 与本地结果不一致 → 检查 viewport / locale / timezone 是否固定(配置里已固定)
  • [ ] 排查失败:npx playwright show-trace test-results/<用例>/trace.zip

参考资料

  • Playwright 官方文档:Auto-waiting、Locators(role/label/test-id)、Browser Contexts、Trace Viewer、CI 配置
  • CI/CD 中浏览器自动化的普及与"失败可诊断性"的价值:2026 State of Web Automation
  • 选择器脆弱性与可断言性痛点的调研结论:IST 2026:Selenium 实践调查

本文由 曲奇 整理,内容由 AI 辅助整理,示例数据为脱敏演示数据;仅供学习参考,转载请注明出处。