Playwright 是微软开源的现代端到端(E2E)测试框架,支持 Chromium、Firefox、WebKit 三大浏览器引擎,用一套 API 即可编写稳定、可靠的浏览器自动化测试。相比传统 Selenium,它的自动等待机制和 Trace 工具让”端到端测试”从劝退变成团队日常。本文从环境搭建、核心定位器、真实登录用例,到 CI 与 Docker 固化,给出一条可立即落地的路径,帮你把端到端测试真正跑进日常研发流程。
一、为什么是 Playwright
在 Playwright 之前,前端自动化测试长期被 Selenium 的”元素找不到就报错””需要手写显式等待”劝退,Puppeteer 又只支持 Chromium。Playwright 用”自动等待(Auto-waiting)”彻底解决了稳定性问题:每个操作在执行前都会自动等待元素达到可操作状态,缺省不再需要 sleep。
| 维度 | Selenium | Puppeteer | Playwright |
|---|---|---|---|
| 支持引擎 | 多(依赖驱动) | 仅 Chromium | Chromium/Firefox/WebKit |
| 自动等待 | 需显式写 | 部分 | 默认全开 |
| 脚本语言 | 多语言 | Node.js | TS/JS/Python/Java/.NET |
| 追踪调试 | 弱 | 一般 | Trace Viewer(强) |
| 网络拦截 | 复杂 | 支持 | 原生 API 支持 |
二、环境搭建与第一个测试
Playwright 官方提供了 Test Runner(@playwright/test),开箱即用,无需额外配置断言库和报告。初始化只需三步:
# 初始化项目并安装
npm init -y
npm i -D @playwright/test
# 安装浏览器二进制(首次需下载,约 100MB+)
npx playwright install chromium
# 生成示例测试与配置
npx playwright test --init
安装完成后,项目根目录会多出 playwright.config.ts 和一个示例 tests/ 目录。直接运行 npx playwright test 即可看到浏览器真实启动并执行用例。
三、核心心智模型:定位器与自动等待
Playwright 推荐使用”定位器(Locator)”而非一次性选择器。Locator 是惰性绑定,每次操作都会重新查询最新 DOM,天然抗重渲染。配合 getByRole、getByLabel 等语义化 API,用例可读性和可维护性都大幅提升。
import { test, expect } from '@playwright/test';
test('自动等待示例', async ({ page }) => {
await page.goto('/login');
// 自动等待输入框可见、可填,无需手写 sleep
await page.getByLabel('用户名').fill('admin');
await page.getByRole('button', { name: '登录' }).click();
// 断言:自动等待 URL 变化
await expect(page).toHaveURL(/dashboard/);
await expect(page.getByText('欢迎回来')).toBeVisible();
});
注意这里没有任何 waitFor 或 sleep:点击会等到按钮可点击,断言会轮询直到通过或超时。这是 Playwright 稳定性的根本来源。
四、实战:登录流程 E2E 用例
下面是一个贴近真实业务的登录流程用例,覆盖”输入凭据 → 提交 → 断言登录态”的完整链路。建议把登录封装成可复用的 fixtures,避免每个用例重复登录。
test('完整登录流程', async ({ page }) => {
await page.goto('https://example.com/login');
await page.fill('#username', 'demo');
await page.fill('#password', 'secret');
await page.click('button[type=submit]');
// 断言登录成功后的关键元素与标题
await expect(page.locator('.avatar')).toBeVisible();
await expect(page).toHaveTitle(/控制台/);
// 网络层断言:拦截并校验关键接口
const resp = await page.waitForResponse('**/api/user/me');
expect(resp.status()).toBe(200);
});
如果系统后端是 FastAPI 或 NestJS 这类接口服务,Playwright 还能顺带完成”接口 + UI”双重校验,把测试金字塔的顶端和中间层打通。
五、调试利器:Trace Viewer 与 VS Code
端到端测试最难的是”本地能过、CI 挂了”。Playwright 的 Trace Viewer 会把每次运行的 DOM 快照、网络、控制台、时间线全部录下来,失败时在浏览器里回放每一步。配合 VS Code 官方插件,可以断点单步、实时查看 Locator 命中情况。
# 录制本次运行的 trace
npx playwright test --trace on
# 本地回放(生成可交互的 HTML 报告)
npx playwright show-report
六、在 CI 中稳定跑起来(GitHub Actions)
端到端测试的价值在于”每次提交都自动验证”,因此必须进 CI。借助 GitHub Actions 的托管 runner,一条 workflow 即可完成”装依赖 → 装浏览器 → 跑测试 → 出报告”。
name: e2e
on: [push, pull_request]
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
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
--with-deps 会自动安装系统级依赖(字体、库文件),避免 CI 环境缺库导致浏览器无法启动——这是新手踩得最多的坑。
七、用 Docker 固化测试环境
为了保证”本地、CI、预发”三套环境行为一致,推荐把测试跑在官方镜像里。Playwright 提供了预装浏览器和依赖的基础镜像,配合 Docker 即可一键复现。
FROM mcr.microsoft.com/playwright:v1.48.0-jammy
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
八、落地清单与常见坑
| 检查项 | 要点 |
|---|---|
| 不要滥用 text 选择器 | 优先 getByRole/getByLabel,抗文案改动 |
| 登录态复用 | 用 setup 项目或 storageState,避免每用例重登 |
| 超时配置 | 在 config 统一设 defaultTimeout,勿散落 sleep |
| 无头模式差异 | 本地 headed 调试,CI 用 headless,注意字体/动画 |
| 偶发抖动 | 开启 trace + retries=2,定位真实失败原因 |
九、参数化与数据驱动测试
端到端测试经常要覆盖多组输入:不同角色账号、多种边界数据、各地区的文案差异。与其复制粘贴十几个几乎相同的用例,不如用 Playwright 的 test 遍历数组,一行声明生成多组测试。这样新增用例只是加一行数据,可读性也更好,后续维护成本显著下降。
const accounts = [
{ role: 'admin', expect: /console/ },
{ role: 'guest', expect: /no-permission/ },
];
for (const a of accounts) {
test(`角色 ${a.role} 登录后跳转正确`, async ({ page }) => {
await login(page, a.role);
await expect(page).toHaveURL(a.expect);
});
}
十、用 Page Object 组织用例
当用例数量增长到几十个,散落各处的选择器会成为维护噩梦:改一个按钮的 id,就要翻遍所有文件。把”页面结构 + 交互方法”封装成 Page Object 类,页面布局变化时只需改动一处。这是 Playwright 社区公认的最佳实践,也让业务逻辑与测试脚本解耦,新同事上手更快。
export class LoginPage {
constructor(private page: import('@playwright/test').Page) {}
async goto() {
await this.page.goto('/login');
}
async signIn(user: string, pwd: string) {
await this.page.fill('#username', user);
await this.page.fill('#password', pwd);
await this.page.click('button[type=submit]');
}
}
十一、拦截网络请求做 Mock
测试第三方支付、地图或天气等外部依赖时,真实调用既慢又不稳定,还可能在夜间被限流。用 page.route 拦截指定接口并伪造响应,用例瞬间变快且百分百可重复。你还能模拟超时、500 等异常分支,把那些”线上偶发、本地难复现”的问题,固化成一条回归用例,价值极高。
await page.route('**/api/price', route =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ price: 99 }),
}),
);
await page.goto('/product');
await expect(page.getByText('¥99')).toBeVisible();
十二、在测试金字塔中的定位
很多团队一上来就大量堆端到端用例,结果流水线跑半小时、还动不动失败,最后干脆不跑了。更健康的做法是遵循测试金字塔:单元测试(最快、数量最多)打底,接口层测试(例如直接用 FastAPI 或 NestJS 自带的测试客户端)覆盖业务规则,端到端只守住最核心的用户旅程,比如登录、下单、支付。三者大致按 70/20/10 配比,既保证覆盖又控制时长。
十三、超时、重试与稳定性收尾
端到端用例偶尔抖动不可避免,关键是把它控制在可接受范围。建议在 playwright.config.ts 里统一设置 timeout 与 retries,并对慢接口单独放宽超时;配合前面提到的 Trace Viewer,失败时能一键回放定位根因。最后把 HTML 报告接入团队的 IM 通知,让”红灯”第一时间被人看见,而不是沉淀在 CI 日志里无人问津。
端到端测试不是越多越好,而是”覆盖关键业务链路 + 稳定不抖”。先把登录、下单、支付这类核心路径用 Playwright 守住,再逐步扩展,团队才会真正信任测试报告。




