Playwright 全自动化 Web 测试详解——从零搭建到生成专业测试报告

一、Playwright 是什么?为什么选择它?

Playwright 是由 Microsoft 开发的现代化端到端(End-to-End, E2E)Web 自动化测试框架,于 2020 年正式开源。它区别于传统 Selenium 的核心优势在于:

  • 自动等待(Auto-Wait)机制 :在执行点击、输入、断言前自动等待元素可见/可交互/稳定,彻底告别 time.sleep()导致的脆弱测试。

  • 全浏览器引擎支持:统一 API 驱动 Chromium(Chrome/Edge)、Firefox、WebKit(Safari),跨平台 Windows/macOS/Linux。

  • 多语言绑定:原生支持 TypeScript/JavaScript(首选)、Python、Java、.NET(C#)。

  • 内置强大工具链:测试代码录制(Codegen)、追踪查看器(Trace Viewer)、UI Mode 调试、内置 HTML/JSON 报告器。

  • 无头/有头模式:默认无头(Headless)运行于 CI,开发时可开启有头模式观察执行过程。

  • 网络拦截与 Mock:可拦截 HTTP 请求、Mock API 响应,方便前后端分离测试。

💡 与 Selenium 对比:Playwright 安装更简单(一条命令下载浏览器)、执行速度更快(基于 DevTools Protocol/CDP 而非 WebDriver 协议)、自动等待减少 Flaky 测试,是现代 Web 应用 E2E 测试的首选。


二、环境搭建

以下分别给出 TypeScript/JavaScript(官方推荐) ​ 和 **Python(pytest + playwright)**​ 两种主流方案的完整搭建步骤。

2.1 Node.js + Playwright(TypeScript)方案

前置条件
  • Node.js ≥ 18(建议 LTS 版本)

  • npm / yarn / pnpm

初始化项目
复制代码
# 创建项目目录
mkdir playwright-demo && cd playwright-demo

# 初始化 Playwright(交互式引导)
npm init playwright@latest

运行 npm init playwright@latest后会询问:

  • 测试目录名(默认 tests/)→ 回车

  • 是否添加 GitHub Actions 工作流 → 可选 Y

  • 是否安装浏览器(Chromium/Firefox/WebKit)→ Y

安装完成后目录结构:

复制代码
playwright-demo/
├── playwright.config.ts   # 核心配置文件
├── package.json
├── tests/
│   └── example.spec.ts   # 示例测试用例
└── tests-examples/       # 更多示例
安装浏览器(如未自动安装)
复制代码
npx playwright install
# 只安装 Chromium
npx playwright install chromium

2.2 Python + pytest-playwright 方案

前置条件
  • Python ≥ 3.8

    安装核心包

    pip install pytest playwright pytest-playwright

    安装浏览器

    playwright install

    (可选)安装报告插件

    pip install allure-pytest pytest-html

Python 方案使用 pytest作为测试运行器,@playwright/test的同步/异步 API 通过 playwright.sync_apiplaywright.async_api调用。


三、第一个自动化测试用例

3.1 TypeScript 版本------百度搜索测试

tests/baidu_search.spec.ts

复制代码
import { test, expect } from '@playwright/test';

test.describe('百度搜索功能', () => {
  test('搜索 Playwright 并验证标题', async ({ page }) => {
    // 1. 打开网页
    await page.goto('https://www.baidu.com');

    // 2. 填写搜索框(Playwright 推荐使用 getByRole / getByPlaceholder)
    await page.getByPlaceholder('请输入搜索关键词').fill('Playwright');
    
    // 3. 点击搜索按钮
    await page.getByRole('button', { name: '百度一下' }).click();

    // 4. 等待导航完成并断言
    await expect(page).toHaveTitle(/Playwright/);
    await expect(page.locator('#content_left')).toBeVisible();
  });
});

运行:

复制代码
# 无头模式运行
npx playwright test

# 有头模式 + 慢动作观察
npx playwright test tests/baidu_search.spec.ts --headed --slowmo=1000

# UI 模式(推荐调试用)
npx playwright test --ui

3.2 Python 版本------百度搜索测试

tests/test_baidu.py

复制代码
import pytest
from playwright.sync_api import Page, expect

def test_baidu_search(page: Page):
    page.goto("https://www.baidu.com")
    page.get_by_placeholder("请输入搜索关键词").fill("Playwright")
    page.get_by_role("button", name="百度一下").click()
    expect(page).to_have_title(/Playwright/)

运行:

复制代码
pytest tests/test_baidu.py --headed

四、Playwright 核心概念详解

4.1 Locators(定位器)------测试稳定性的关键

Playwright 推荐按用户可感知的特征定位,优先级如下:

推荐度 方式 示例
⭐⭐⭐⭐⭐ 最推荐 getByRole page.getByRole('button', { name: '提交' })
⭐⭐⭐⭐⭐ getByLabel page.getByLabel('用户名').fill('admin')
⭐⭐⭐⭐⭐ getByPlaceholder page.getByPlaceholder('请输入密码')
⭐⭐⭐⭐ getByText page.getByText('登录成功').click()
⭐⭐⭐ getByTestId(需加 data-testid) page.getByTestId('submit-btn')
⚠️ 不推荐 CSS Selector / XPath page.locator('#kw')------ 易随 UI 变动失效

✅ 最佳实践:与前端约定给关键元素加 data-testid="xxx",用 getByTestId定位,兼顾语义稳定和抗重构。

4.2 Actions(操作)

复制代码
// 点击
await page.getByRole('button', { name: '登录' }).click();

// 输入(逐字符输入,可带延迟)
await page.getByLabel('密码').fill('123456');

// 双击 / 右键
await page.getByText('Item').dblclick();
await page.getByText('Item').click({ button: 'right' });

// 下拉框
await page.getByRole('combobox').selectOption('value2');

// 文件上传
await page.getByLabel('上传头像').setInputFiles('avatar.png');

// 键盘 / 鼠标
await page.keyboard.press('Control+A');
await page.mouse.wheel(0, 500);

4.3 Assertions(断言)

Playwright 使用 expect配合自动重试断言(Soft Assert 也支持)

复制代码
// 元素可见
await expect(page.getByText('欢迎')).toBeVisible();
// URL 匹配
await expect(page).toHaveURL(/dashboard/);
// 输入框值
await expect(page.getByLabel('邮箱')).toHaveValue('a@b.com');
// 元素包含文本
await expect(page.locator('.msg')).toContainText('保存成功');
// 元素不存在
await expect(page.getByText('错误提示')).not.toBeVisible();

// 软断言(一个失败不阻断后续检查)
await expect.soft(page.getByText('A')).toBeVisible();
await expect.soft(page.getByText('B')).toBeVisible();

4.4 Fixtures 与 Hooks

复制代码
test.beforeAll(async ({ browser }) => {
  // 整个 describe 块前执行一次,适合全局登录获取 token
});

test.beforeEach(async ({ page }) => {
  // 每个测试前执行,常用跳转登录页
  await page.goto('/login');
  await page.fill('#user', 'admin');
  await page.fill('#pass', '123456');
  await page.click('button[type=submit]');
});

test.afterEach(async ({ page }) => {
  // 每个测试后清理
});

test.afterAll(async () => {
  // 全部结束后执行
});

{ page }{ browser }{ context }{ request }(API 测试)均为 Playwright 内置 Fixture,自动注入。


五、Page Object Model(POM)------可维护框架基石

随着用例增多,直接把定位器和操作写在 spec 文件中会导致大量重复代码。Page Object Model​ 将每个页面封装为一个类:

目录结构示例

复制代码
tests/
├── login.spec.ts
├── dashboard.spec.ts
└── pages/
    ├── LoginPage.ts
    └── DashboardPage.ts

pages/LoginPage.ts

复制代码
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly loginBtn: Locator;
  readonly errorMsg: Locator;

  constructor(page: Page) {
    this.page = page;
    this.usernameInput = page.getByLabel('用户名');
    this.passwordInput = page.getByLabel('密码');
    this.loginBtn = page.getByRole('button', { name: '登录' });
    this.errorMsg = page.locator('.error-message');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(user: string, pwd: string) {
    await this.usernameInput.fill(user);
    await this.passwordInput.fill(pwd);
    await this.loginBtn.click();
  }

  async assertLoginSuccess() {
    await expect(this.page).toHaveURL(/dashboard/);
  }

  async assertLoginError(msg: string) {
    await expect(this.errorMsg).toHaveText(msg);
  }
}

使用 POM 编写测试

复制代码
import { test } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';

test('正常登录', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('admin', '123456');
  await loginPage.assertLoginSuccess();
});

test('密码错误提示', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('admin', 'wrong');
  await loginPage.assertLoginError('用户名或密码错误');
});

POM 使定位器变更只需改一处,测试代码只描述业务流,清晰且可复用。


六、测试报告生成------重点!

Playwright 内置多种 Reporter,也支持第三方 Allure 报告。

6.1 内置 HTML 报告(最简单,开箱即用)

playwright.config.ts中配置:

复制代码
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [['html', { open: 'never', outputFolder: 'playwright-report' }]],
  // 失败时自动截图 + 追踪
  use: {
    screenshot: 'only-on-failure',
    trace: 'retain-on-failure',
    video: 'retain-on-failure',
  },
});

运行后查看:

复制代码
npx playwright test
npx playwright show-report

内置 HTML 报告支持:按浏览器/状态筛选、查看失败截图、下载 Trace ZIP 进行时间旅行调试。

6.2 Allure 报告(企业级,图形化趋势+分类)

TypeScript 项目集成 Allure
复制代码
npm install -D allure-playwright allure-commandline

playwright.config.ts

复制代码
reporter: [
  ['list'],
  ['allure-playwright', { resultsDir: 'allure-results' }],
],

运行与生成报告:

复制代码
# 执行测试
npx playwright test

# 生成并打开报告
allure generate allure-results --clean -o allure-report
allure open allure-report
# 或直接 serve
allure serve allure-results

在用例中添加 Allure 元数据:

复制代码
import * as allure from 'allure-js-commons';

test('登录功能 - 正常', async ({ page }) => {
  await allure.epic('用户认证');
  await allure.feature('登录');
  await allure.story('有效账号登录');
  await allure.severity('critical');
  await allure.owner('QA Team');
  
  await page.goto('/login');
  // ... 测试步骤
  const shot = await page.screenshot();
  await allure.attachment('登录页截图', shot, 'image/png');
});
Python + pytest 集成 Allure
复制代码
pip install allure-pytest
pytest --alluredir=allure-results
allure serve allure-results

6.3 pytest-html(Python 轻量报告)

复制代码
pip install pytest-html
pytest --html=report.html --self-contained-html

七、playwright.config.ts 核心配置详解

复制代码
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // 全局超时(单用例默认 30s)
  timeout: 30000,
  
  // 并行 worker 数(CI 可调大)
  workers: process.env.CI ? 4 : undefined,
  
  // 测试文件匹配规则
  testMatch: '**/*.spec.ts',
  
  // 多浏览器项目
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
  
  // 基础 URL(page.goto('/login') 会拼此地址)
  use: {
    baseURL: 'http://localhost:3000',
    headless: !process.env.HEADED,
    viewport: { width: 1280, height: 720 },
    // 自动截图/trace/video
    screenshot: 'only-on-failure',
    trace: 'retain-on-failure',
    video: 'retain-on-failure',
    // 全局 HTTP 凭证(内网系统)
    // httpCredentials: { username: 'user', password: 'pass' },
  },
  
  // 报告器
  reporter: [
    ['list'],
    ['html', { open: 'never' }],
  ],
  
  // 全局 setup(如共享登录获取 storageState)
  // globalSetup: require.resolve('./global-setup'),
  // storageState: 'storageState.json',
});

八、高级特性

8.1 Codegen------录制生成测试代码

无需手写初始代码,让 Playwright 录制你的操作:

复制代码
npx playwright codegen https://www.baidu.com

浏览器中你做的每一步点击/输入,Inspector 都会生成对应代码,可复制进 .spec.ts。注意录制不会自动加断言,需手动补充 expect

8.2 Trace Viewer------时间旅行调试

复制代码
// 在 beforeEach 中开启
await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
// 在 afterEach(失败时)停止
await context.tracing.stop({ path: 'trace.zip' });

# 查看
npx playwright show-trace trace.zip

可拖拽时间轴查看每步 DOM 快照、网络请求、控制台日志,是排查 Flaky 测试的神器。

8.3 API 测试(混合测试)

Playwright 内置 requestFixture 可做接口测试或 Mock:

复制代码
test('API 获取用户列表', async ({ request }) => {
  const res = await request.get('/api/users');
  expect(res.ok()).toBeTruthy();
  const body = await res.json();
  expect(body.data).toHaveLength(3);
});

8.4 失败重试与超时调优

复制代码
# 失败自动重试 2 次(适合偶尔网络抖动)
npx playwright test --retries=2

# 只运行标记 smoke 的用例
npx playwright test --grep @smoke

在 config 中配置 retries: 2projects可单独设不同重试次数。


九、CI/CD 集成(GitHub Actions 示例)

.github/workflows/e2e.yml

复制代码
name: Playwright E2E Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

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/

每次 Push 自动跑测试并上传 HTML 报告制品。


十、最佳实践总结

维度 建议
定位策略 优先 getByRole/getByLabel/getByTestId,避免脆弱 CSS/XPath
等待 expect(...).toBeVisible()自动等待,禁用 sleep
测试隔离 每个 test 独立,用 beforeEach初始化,不依赖前序用例执行结果
登录态复用 globalSetup+ storageState.json保存 Cookie/Token,加速用例
断言丰富度 断言 URL、文本、属性、可见性,不止截图对比
报告 开发阶段用内置 HTML;团队交付用 Allure(含趋势、分类、附件)
失败分析 开启 trace: 'retain-on-failure'+ screenshot: 'only-on-failure'
稳定性 配置 --retries=2应对偶发网络抖动,配合 Allure @flaky标记

十一、快速上手命令速查

复制代码
# 初始化
npm init playwright@latest

# 运行全部
npx playwright test

# 有头模式运行单个文件
npx playwright test tests/login.spec.ts --headed

# UI 模式(强烈推荐)
npx playwright test --ui

# 录制
npx playwright codegen https://example.com

# 查看报告
npx playwright show-report

# Allure(TS)
allure serve allure-results