首页
/ Playwright Python 测试编写实战:自动等待、Locator 定位、expect 断言与 pytest 夹具

Playwright Python 测试编写实战:自动等待、Locator 定位、expect 断言与 pytest 夹具

2026-09-06 17:37:39作者:房伟宁

本文以 Playwright 官方 Python 文档 writing-tests-python.md 为核心,系统讲解 Python 版 Playwright 测试的完整编写方法:如何用自动等待(actionability)消除手写 sleep、如何用 Locator API 定位页面元素、如何用 expect 断言做自动重试式校验,以及 pytest 夹具(fixtures)如何实现测试隔离与前后置逻辑。读完本文,你能够独立写出稳定、无 flaky 超时问题的 Playwright Python 端到端测试,并理解其底层等待与重试机制在源码中的实现位置。

设计哲学:执行动作 + 断言状态

Playwright 测试遵循两条极简原则:

  • 执行动作(perform actions):对页面元素发起点击、填表、按键等操作;
  • 断言状态(assert the state):对页面或元素状态进行符合预期的校验。

与传统框架不同,Playwright 在这两个环节上都消除了"等待"的心智负担:

  1. 执行动作前无需等待。Playwright 会在每次动作执行前,自动等待一系列 可操作性问题检查(actionability checks) 全部通过后才真正执行动作;
  2. 断言时不存在竞态条件。Playwright 的断言被设计为"描述一个最终必须满足的期望",assertion 会持续自动重试,直到期望条件成立或超时。

官方文档明确指出,这两项设计选择让使用者"彻底忘记 flaky 超时与竞态检查"。这一点在底层源码中可以直接印证:服务端在 packages/playwright-core/src/server/dom.ts 中实现了动作前的 performActionChecks 检查流程,客户端的 waitForElementState 则位于 packages/playwright-core/src/client/elementHandle.ts;相关的行为验证测试见 tests/page/elementhandle-wait-for-element-state.spec.ts

第一个测试

先看一个完整的测试文件示例。注意两个 Python 命名约定:文件名以 test_ 前缀开头测试函数名同样以 test_ 开头,pytest 依赖这一约定来发现测试:

import re
from playwright.sync_api import Page, expect

def test_has_title(page: Page):
    page.goto("https://playwright.dev/")

    # Expect a title "to contain" a substring.
    expect(page).to_have_title(re.compile("Playwright"))

def test_get_started_link(page: Page):
    page.goto("https://playwright.dev/")

    # Click the get started link.
    page.get_by_role("link", name="Get started").click()

    # Expects page to have a heading with the name of Installation.
    expect(page.get_by_role("heading", name="Installation")).to_be_visible()

示例要点:

  • 测试函数直接以参数形式接收 page 夹具(见后文"测试隔离"一节),无需自行创建浏览器或页面;
  • expect(page).to_have_title(re.compile("Playwright")) 使用正则匹配标题子串,且会自动等待直到标题出现;
  • page.get_by_role("link", name="Get started") 是角色定位器,等价于用户视角的"名为 Get started 的链接"。

动作(Actions)

导航:从 page.goto 开始

大多数测试都以导航到 URL 开始,之后才能与页面元素交互:

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

page.goto 会等待页面到达 load 状态后才继续执行后续步骤,因此不需要在导航后再手动等待 DOM 就绪。更多参数(如 wait_untiltimeout 等)见 API 参考 class-page.md 中的 Page.goto

交互:一切从定位元素开始

执行动作的第一步是定位元素。Playwright 使用 Locator API 完成这件事。Locator 代表"在任意时刻都能找到页面中某个(些)元素"的方式,它是 Playwright 自动等待与重试能力的核心组件。Playwright 会等待元素变为可操作(actionable)后才执行动作,因此无需手动等待元素出现。

# Create a locator.
get_started = page.get_by_role("link", name="Get started")

# Click it.
get_started.click()

在大多数情况下,定位与动作会合并写成一行:

page.get_by_role("link", name="Get started").click()

Locator 是"惰性"的:每次使用 locator 执行动作时,都会在页面中重新解析出最新的 DOM 元素。例如 hover()click() 之间如果发生重新渲染,click() 作用于的是重新解析后的新元素,而不是悬停时的旧节点。这一点有专门的测试覆盖:tests/page/retarget.spec.ts 验证了 locator/element handle 在 DOM 变化后"重新指向"最新元素的行为。

Locator 的完整类型与用法(get_by_roleget_by_labelget_by_textget_by_placeholderget_by_alt_textget_by_titleget_by_test_id、CSS/XPath、Shadow DOM 穿透、filter 过滤等)请参见 locators.md

常用基础动作清单

以下是最常用的 Playwright 动作。还有更多动作,完整的 API 请查阅 Locator API 参考

动作(Python 方法) 说明
Locator.check() 勾选复选框
Locator.click() 点击元素
Locator.uncheck() 取消勾选复选框
Locator.hover() 鼠标悬停到元素上
Locator.fill() 填写表单字段、输入文本
Locator.focus() 聚焦元素
Locator.press() 按下单个按键
Locator.set_input_files() 选择文件用于上传
Locator.select_option() 在下拉框中选择选项

动作前的 actionability 检查:等待从何而来

"自动等待"的具体含义是:每次动作执行前,Playwright 会对元素执行一组检查,只有全部通过才执行动作;若在规定 timeout 内检查未通过,动作以 TimeoutError 失败。例如对 Locator.click(),Playwright 会确保:

  • locator 恰好解析为一个元素;
  • 元素是可见的(Visible);
  • 元素是稳定的(Stable),即不在动画中或动画已完成;
  • 元素能接收事件(Receives Events),即没有被其他元素遮挡;
  • 元素是启用的(Enabled)。

完整检查矩阵(摘自 actionability.md):

动作 Visible Stable Receives Events Enabled Editable
Locator.check() Yes Yes Yes Yes -
Locator.click() Yes Yes Yes Yes -
Locator.dblclick() Yes Yes Yes Yes -
Locator.set_checked() Yes Yes Yes Yes -
Locator.tap() Yes Yes Yes Yes -
Locator.uncheck() Yes Yes Yes Yes -
Locator.hover() Yes Yes Yes - -
Locator.drag_to() Yes Yes Yes - -
Locator.screenshot() Yes Yes - - -
Locator.fill() Yes - - Yes Yes
Locator.clear() Yes - - Yes Yes
Locator.select_option() Yes - - Yes -
Locator.scroll_into_view_if_needed() - Yes - - -
Locator.blur() / Locator.dispatch_event() / Locator.focus() / Locator.press() / Locator.press_sequentially() / Locator.set_input_files() - - - - -

各项检查的精确定义:

  • Visible(可见):元素具有非空 bounding box,且没有 visibility:hidden 计算样式。注意:零尺寸元素、display:none 元素不算可见;而 opacity:0 的元素可见;
  • Stable(稳定):元素在至少连续两个动画帧内保持相同的 bounding box;
  • Enabled(启用):元素未被禁用。<button><select><input><textarea><option><optgroup>[disabled] 属性、处于带 [disabled]<fieldset> 内、或是 [aria-disabled=true] 元素的后代,均视为禁用;
  • Editable(可编辑):元素已启用且非 readonly。带 [readonly] 属性的表单元素,或带 [aria-readonly=true] 且角色支持该属性的元素,视为只读;
  • Receives Events(接收事件):元素是指定动作点的命中标的。例如在点 (10;10) 处点击时,Playwright 会检查是否有其他元素(通常是覆盖层)会先截获该位置的点击。

典型场景:页面在查询用户名唯一性期间 Sign Up 按钮处于 disabled 状态,查询完成后被替换为启用的按钮。无论 Locator.click() 调用发生在何时,Playwright 都会等待并点击到最终启用的按钮——不需要任何手写等待。

强制动作:部分动作(如 click)支持 force 选项,可禁用非关键检查。例如 click 传入 truthy 的 force 后,将不再检查目标元素是否真正能接收到点击事件。

断言(Assertions)

Playwright 内置自动重试断言,会一直等待直到期望条件满足。使用这些断言可以让测试不 flaky、具备韧性。例如,下面这行代码会等待直到页面标题包含 "Playwright":

import re
from playwright.sync_api import expect

expect(page).to_have_title(re.compile("Playwright"))

最常用的断言清单(还有更多,完整列表见 actionability.md断言参考):

断言(Python 方法) 说明
LocatorAssertions.to_be_checked() 复选框已勾选
LocatorAssertions.to_be_enabled() 控件已启用
LocatorAssertions.to_be_visible() 元素可见
LocatorAssertions.to_contain_text() 元素包含文本
LocatorAssertions.to_have_attribute() 元素具有某属性
LocatorAssertions.to_have_count() 元素列表长度为指定值
LocatorAssertions.to_have_text() 元素匹配文本
LocatorAssertions.to_have_value() 输入元素具有某值
PageAssertions.to_have_title() 页面具有标题
PageAssertions.to_have_url() 页面具有 URL

与 JS 版的 await expect(...).toBeVisible() 不同,Python 同步 API 中 expect(...).to_be_visible() 是同步调用,内部通过 sync_api 的消息泵实现阻塞式自动重试;异步 API(playwright.async_api)中则写作 await expect(...).to_be_visible()

测试隔离(Test isolation)

Playwright 的 pytest 插件基于 pytest 夹具体系,其中最核心的是内置的 page 夹具。页面之间的隔离由 Browser Context 保证——每个 Browser Context 相当于一个全新的浏览器配置文件(profile):每个测试都获得干净环境(无 Cookie、无缓存、无 localStorage),即使多个测试运行在同一个 Browser 实例中。关于 Browser Context 的更多概念见 browser-contexts.md

from playwright.sync_api import Page

def test_example_test(page: Page):
  pass
  # "page" belongs to an isolated BrowserContext, created for this specific test.

def test_another_test(page: Page):
  pass
  # "page" in this second test is completely isolated from the first test.

换言之:第一个测试对页面做的任何修改(登录态、Cookie、DOM 改动)都不会泄漏到第二个测试。

使用 pytest 夹具(Fixtures)

可以利用各种 pytest fixtures 在测试前后执行代码、并在测试间共享对象。官方文档给出的关键对照:

  • function 作用域的 autouse 夹具 行为等价于 beforeEach/afterEach——每个测试前后各运行一次;
  • module 作用域的 autouse 夹具 行为等价于 beforeAll/afterAll——在模块内所有测试之前与之后各运行一次。
import pytest
from playwright.sync_api import Page, expect

@pytest.fixture(scope="function", autouse=True)
def before_each_after_each(page: Page):

    print("before the test runs")

    # Go to the starting url before each test.
    page.goto("https://playwright.dev/")
    yield

    print("after the test runs")

def test_main_navigation(page: Page):
    # Assertions use the expect API.
    expect(page).to_have_url("https://playwright.dev/")

yield 之前的代码在每个测试前执行,yield 之后的代码在每个测试结束后执行(包括测试失败时),非常适合"进入起始页 / 清理状态"这类前后置逻辑。

插件内置夹具速查

除自定义夹具外,pytest 插件(pytest-playwright)还内置了一组 Playwright 专属夹具,完整参考见 test-runners-python.md

函数作用域(测试函数请求时创建,测试结束即销毁):

  • context:测试专用的新 Browser Context;
  • page:测试专用的新页面;
  • new_context:创建多个不同 Browser Context 的回调,适合多用户场景,参数与 Browser.newContext 一致。

会话作用域(所有测试结束后销毁):

  • playwright:Playwright 实例;
  • browser_type / browser / browser_name / browser_channel:浏览器类型、已启动的浏览器实例及名称/渠道字符串;
  • is_chromiumis_webkitis_firefox:对应浏览器类型的布尔值,可用于按浏览器条件跳过测试。

自定义启动/上下文选项:通过覆盖夹具 browser_type_launch_args(覆盖 BrowserType.launch 参数)、browser_context_args(覆盖 Browser.newContext 选项)或 connect_options(通过 WebSocket 端点连接已有浏览器)实现。例如在 conftest.py 中统一设置视口:

# conftest.py
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        "viewport": {"width": 1920, "height": 1080},
    }

也可以只针对单个测试覆盖上下文选项:

import pytest

@pytest.mark.browser_context_args(timezone_id="Europe/Berlin", locale="en-GB")
def test_browser_context_args(page):
    assert page.evaluate("window.navigator.languages") == ["de-DE"]

注意一个适用前提:命令行参数(如 --headed--browser firefox只作用于默认 browsercontextpage 夹具;如果你通过 Browser.newContext() 之类的 API 自行创建浏览器/上下文,CLI 参数不会生效。

接下来学什么

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