首页
/ Playwright 测试编写实战:从第一个测试到断言、隔离与 Hooks 的完整指南

Playwright 测试编写实战:从第一个测试到断言、隔离与 Hooks 的完整指南

2026-09-06 17:34:18作者:韦蓉瑛

本文基于 Playwright 官方文档 writing-tests(JavaScript 版),系统讲解如何编写 Playwright 测试:编写第一个测试、执行页面动作(导航与交互)、使用异步断言、利用 BrowserContext 实现测试隔离,以及用 test.describe / test.beforeEach 等 hooks 组织测试。读完本文,你将掌握 Playwright 测试的核心编写模式,并能结合仓库源码理解这些 API 背后的自动等待与轮询机制。

测试编写总览:执行动作并断言状态

Playwright 测试的核心范式非常简单:执行动作(perform actions),然后**断言状态(assert the state)**是否符合预期。

它有两个关键设计,决定了测试编写的整体风格:

  1. 动作前的自动等待:Playwright 在执行每个动作前,会自动等待 actionability(可操作性) 检查通过,你不需要手写任何 waitForTimeout,也不用担心竞态条件。
  2. 面向“终将成立”的断言:Playwright 的断言(expectations)被设计为描述“最终会被满足”的预期条件,通过自动重试轮询消除不稳定的超时和竞态检查。

官方文档给出的学习目标(也是本文的章节脉络):

  • 如何编写第一个测试
  • 如何执行动作(Actions)
  • 如何使用断言(Assertions)
  • 测试如何以隔离方式运行(Test Isolation)
  • 如何使用测试 hooks

第一个测试

先看一个完整示例,覆盖“导航 + 标题断言 + 点击 + 可见性断言”的最小闭环。文件保存为 tests/example.spec.ts

import { test, expect } from '@playwright/test';

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Expect a title "to contain" a substring.
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Click the get started link.
  await page.getByRole('link', { name: 'Get started' }).click();

  // Expects page to have a heading with the name of Installation.
  await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});

要点说明:

  • testexpect 都从 @playwright/test 包导入,这是测试入口(对应仓库中的 packages/playwright)。
  • 测试函数签名是 async ({ page }) =>page 是 Playwright 内置的 fixture(测试夹具),由框架自动创建并注入,无需手动 browser.newPage()
  • toHaveTitle(/Playwright/) 接受正则,做的是“标题包含子串”的匹配,而非精确匹配。

提示(来自原文档):在 VS Code 中使用 JavaScript(而非 TypeScript)编写测试时,在每个测试文件开头添加 // @ts-check,即可获得自动类型检查。

动作(Actions):导航与交互

导航(Navigation)

大多数测试从导航到某个 URL 开始,之后再与页面元素交互:

await page.goto('https://playwright.dev/');

Python 版本对应写法(跨语言参考):

page.goto("https://playwright.dev/")

page.goto 会等待页面达到 load 状态后才继续,避免“页面还没加载就开始操作”的问题。完整参数(如 waitUntiltimeout 等)可查阅 Page.goto API

交互(Interactions):基于 Locator 定位元素

执行动作的第一步是定位元素,Playwright 使用 Locators API 来完成。Locator 代表“在任意时刻都能找到页面元素”的一种惰性查询——它不绑定某个具体 DOM 节点,而是绑定一段查找逻辑,因此天然适配动态更新的页面。定位器的种类(getByRolegetByTextgetByLabel 等)见 Locators 指南

Playwright 在执行动作前会等待元素变为 actionable(可操作)(可见、稳定、可接收事件、未被其他元素遮挡等),所以不需要自己等待元素“出现”:

// Create a locator.
const getStarted = page.getByRole('link', { name: 'Get started' });

// Click it.
await getStarted.click();

大多数场景会直接写成一行链式调用:

await page.getByRole('link', { name: 'Get started' }).click();

常用基础动作一览

以下是 Playwright 中最常用的动作,完整列表见 Locator API

动作 说明
Locator.check 勾选复选框
Locator.click 点击元素
Locator.uncheck 取消勾选复选框
Locator.hover 鼠标悬停在元素上
Locator.fill 填充表单字段 / 输入文本
Locator.focus 聚焦元素
Locator.press 按下单个按键
Locator.setInputFiles 选择文件上传
Locator.selectOption 在下拉框中选择选项

断言(Assertions):异步等待型断言消除不稳定

Playwright 内置的测试断言以 expect 函数的形式提供:调用 expect(value) 后选择一个匹配器(matcher)来表达预期。其实现入口在 matchers/expect.ts,由 packages/playwright/src/index.ts 统一导出。

异步匹配器:等待直到条件成立

Playwright 包含异步匹配器,它们会持续重试,直到预期条件被满足(或超时)。使用这些匹配器是测试不 flaky、具备韧性的关键。例如,下面这行代码会等待直到页面标题包含 "Playwright":

await expect(page).toHaveTitle(/Playwright/);

以下是最常用的异步断言,完整列表见 断言指南:

断言 说明
LocatorAssertions.toBeChecked 复选框已被勾选
LocatorAssertions.toBeEnabled 控件处于启用状态
LocatorAssertions.toBeVisible 元素可见
LocatorAssertions.toContainText 元素包含指定文本
LocatorAssertions.toHaveAttribute 元素具有指定属性
LocatorAssertions.toHaveCount 元素列表具有指定长度
LocatorAssertions.toHaveText 元素匹配指定文本
LocatorAssertions.toHaveValue 输入元素具有指定值
PageAssertions.toHaveTitle 页面具有指定标题
PageAssertions.toHaveURL 页面处于指定 URL

从源码看异步断言的实现

在仓库中,这些异步匹配器都定义在 packages/playwright/src/matchers/matchers.ts。以 toBeVisibletoBeEnabled 为例(第 185 行、第 152 行附近):

// packages/playwright/src/matchers/matchers.ts
export function toBeVisible(
  this: ExpectMatcherStateInternal,
  locator: LocatorEx,
  options?: { visible?: boolean, timeout?: number, signal?: AbortSignal },
) {
  const visible = !options || options.visible === undefined || options.visible;
  const expected = visible ? 'visible' : 'hidden';
  // ...
  return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', expected, arg, async (isNot, timeout, signal) => {
    return await locator._expect(visible ? 'to.be.visible' : 'to.be.hidden', { isNot, timeout, signal, title: this.title });
  }, options);
}

从源码结构看,每个 Locator 断言最终都通过 locator._expect(...) 委托给注入到页面的 utility script 在浏览器端执行检查——这正是断言能够“反复轮询直到条件成立”的底层机制。另外值得注意的是:

  • toBeVisible / toBeEnabled / toBeChecked 都支持反向参数(如 { visible: false }{ enabled: false }{ checked: false }),一个匹配器即可覆盖正反两种预期;
  • 多数匹配器支持 timeoutAbortSignal 参数,用于覆盖默认超时或响应取消。

断言超时的默认值与优先级

packages/playwright/src/matchers/expect.ts 中可以确认异步断言的默认超时与解析优先级:

const defaultExpectTimeout = 5000;
// ...
const timeout = info.timeout ?? expectConfig().timeout ?? defaultExpectTimeout;

即:匹配器自身的 timeout 参数 > expect.configure({ timeout }) 全局配置 > 默认值 5000ms。这意味着若页面条件在 5 秒内无法成立,断言才会失败;你可以按项目需要调整其中任何一级。

通用匹配器:同步检查已就绪的值

Playwright 还包含 toEqualtoContaintoBeTruthy通用匹配器,用于断言任意条件。与异步匹配器不同,它们执行的是对已就绪值的立即同步检查,因此**不需要 await

expect(success).toBeTruthy();

适用边界:通用匹配器适合对同步计算结果(如函数返回值、配置值)做即时检查;对页面状态(元素是否存在、文本是否出现)则应优先使用带 await 的异步匹配器。

测试隔离(Test Isolation):每个测试都是全新环境

Playwright Test 建立在 test fixtures(测试夹具) 的概念之上,例如传入测试的内置 page fixture。关键在于:页面通过 BrowserContext 在测试之间相互隔离——一个 BrowserContext 相当于一个全新的浏览器配置(干净的 Cookie、localStorage、缓存),因此即使多个测试运行在同一个浏览器进程内,每个测试拿到的也是一个全新环境。

import { test } from '@playwright/test';

test('example test', async ({ page }) => {
  // "page" belongs to an isolated BrowserContext, created for this specific test.
});

test('another test', async ({ page }) => {
  // "page" in this second test is completely isolated from the first test.
});

实践含义:

  • 测试之间不共享状态,顺序无关,可安全并行;
  • 登录等一次性操作不适合写在每个测试里反复执行,应通过 fixtures 或 storage state 复用,避免重复成本(详见 BrowserContexts 指南);
  • 隔离是以 BrowserContext 为粒度实现的,而非为每个测试启动新浏览器,因此在性能上开销可控。

使用测试 Hooks:组织与复用

Playwright 提供了多种 test hooks

Hook 执行时机
test.describe 声明一组测试(测试分组)
test.beforeEach 每个测试执行之前
test.afterEach 每个测试执行之后
test.beforeAll 每个 worker 中所有测试执行之前(仅一次)
test.afterAll 每个 worker 中所有测试执行之后(仅一次)

注意 beforeAll / afterAll每 worker 执行一次,而非整个测试文件只执行一次——在并行执行下,同一文件的不同测试可能被拆分到不同 worker。

import { test, expect } from '@playwright/test';

test.describe('navigation', () => {
  test.beforeEach(async ({ page }) => {
    // Go to the starting url before each test.
    await page.goto('https://playwright.dev/');
  });

  test('main navigation', async ({ page }) => {
    // Assertions use the expect API.
    await expect(page).toHaveURL('https://playwright.dev/');
  });
});

这种“beforeEach 准备环境 + describe 按功能分组”的组织方式是官方示例项目的一贯写法。仓库中的 examples/todomvc/tests 即按 adding-todoscompleting-todosdeleting-todosediting-todosfiltering-todostodo-creation 等目录分模块组织,并配套公共 fixtures.ts 来复用测试前置逻辑,可作为大型测试套件的参考结构。

后续学习路径

按照原文档给出的 What's Next,编写完测试后可以继续:

登录后查看全文
热门项目推荐
相关项目推荐