首页 / Playwright 入门教程 / Python 版入门与差异

Playwright 入门教程

Python 版入门与差异

本教程共 59 篇 · 第 57 篇 · 更新于 2026-08-04 · 约 11 分钟阅读

PlaywrightPythonpytestpytest-playwright夹具Page Object

本节目标:装好 Python 版 Playwright,写出第一个 pytest 测试,并说清它跟 TS 版哪里不一样。

Python 版没有自己的测试运行器。它的套路是「Playwright 库 + pytest + 官方插件」。

三样东西各管一摊,搞清分工,后面就不迷糊。

安装

先建虚拟环境。Python 项目一律这么起手,避免依赖打架。

# 1. 建虚拟环境
python3 -m venv venv

# 2. 激活(macOS / Linux)
source venv/bin/activate

# 3. 激活(Windows)
venv\Scripts\activate

提示符前面出现 (venv),就说明激活成功了。

接着装包。装 pytest-playwright 会连带把 playwrightpytest 一起拉下来。

pip install pytest-playwright

最后装浏览器。这一步下载 Chromium、Firefox、WebKit 三套二进制。

playwright install
Note

pip install playwright 只装库,不装浏览器;playwright install 才是下载浏览器。两条命令名字像,作用完全不同,新手常在这里卡住。

版本方面,Python 包跟主线同步,1.62.x 对应本教程基线。Python 解释器建议 3.9 以上。

第一个测试

pytest 靠命名约定发现测试:文件叫 test_*.py,函数叫 test_*。没有 test() 包装函数这回事。

tests/test_search.py 里写:

from playwright.sync_api import Page, expect

def test_open_homepage(page: Page) -> None:
    page.goto("https://playwright.dev/")
    expect(page).to_have_title("Fast and reliable end-to-end testing for modern web apps | Playwright")

这里的 page 是个夹具(Fixture,夹具),由 pytest-playwright 插件提供。只要写进函数参数,插件就会自动注入。

跑起来:

python3 -m pytest tests
Tip

python3 -m pytest 而不是直接 pytest。前者会把当前目录加进 Python 搜索路径,后面导入自己写的模块才不会报 ModuleNotFoundError

调试时加两个参数,能看见浏览器慢动作演示:

python3 -m pytest tests --headed --slowmo 1000

插件提供了哪些夹具

TS 版里 pagecontext 是测试运行器内置的。Python 版由插件提供,作用域各不相同。

夹具作用作用域
browser浏览器实例session(全部测试共用一个)
context浏览器上下文,相当于无痕窗口function(每个测试一个新的)
page页面标签页function(每个测试一个新的)
browser_name当前浏览器名字function

一个浏览器、多个浏览器上下文,这是 Playwright 的隔离模型。浏览器上下文创建成本极低,所以每个测试单独一个也不心疼。

用完不需要手动关。夹具会自己收尾。

同步 API 和异步 API

Python 版给了两套 API,这是它跟其他语言最大的不同。

# 同步版:写测试用这个
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://playwright.dev/")
    print(page.title())
    browser.close()
# 异步版:爬虫、并发抓取用这个
import asyncio
from playwright.async_api import async_playwright

async def main() -> None:
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://playwright.dev/")
        print(await page.title())
        await browser.close()

asyncio.run(main())

异步版每个调用都要 async defawait,函数还得用 asyncio.run 启动。

写端到端测试就用同步版。测试本来就是一步接一步,异步带来的并发收益几乎为零,反而多了一堆 await

Warning

pytest-playwright 提供的是同步夹具。想在 pytest 里写异步测试,要换成官方的 pytest-playwright-asyncio 插件,两者不要同时装。

命名与传参差异

Python 版把 API 翻译成了地道的 Python 写法,主要是两条规则。

方法名蛇形化getByRoleget_by_roleallTextContentsall_text_contents

选项对象变关键字参数:TS 里传一个对象,Python 直接展开成命名参数。

# TS 写法:page.goto(url, { waitUntil: 'networkidle' })
page.goto("https://playwright.dev/", wait_until="networkidle")

# TS 写法:page.getByRole('button', { name: 'Sign in' })
page.get_by_role("button", name="Sign in")

# TS 写法:expect(locator).toBeVisible({ timeout: 10000 })
expect(locator).to_be_visible(timeout=10000)

记住这两条,TS 文档里的示例基本能一比一翻译过来。

断言:expect 和 assert 的区别

Python 自带 assert,pytest 也鼓励用它。但在 Playwright 里,这两种断言性质完全不同。

from playwright.sync_api import expect

# 推荐:Web 优先断言,会自动重试
expect(page.locator("#search_form_input")).to_have_value("panda")

# 不推荐:取一次值就判断,取到啥算啥
assert page.input_value("#search_form_input") == "panda"

expect 会在超时窗口内反复检查,直到条件成立。页面还在加载也没关系,它等得起。

assert 只看当下这一瞬间。值还没填进去就判断,测试直接红。

规矩很简单:凡是跟页面状态有关的断言,一律用 expect。纯 Python 数据的判断(比如列表长度)才用 assert

titles = page.locator('a[data-testid="result-title-a"]').all_text_contents()
matches = [t for t in titles if "panda" in t.lower()]
assert len(matches) > 0   # 这是普通列表判断,用 assert 合适

夹具:pytest 的 conftest.py 取代 test.extend

TS 版用 test.extend() 扩展夹具。Python 版没有这个东西,走 pytest 自己那套。

共享夹具放在 tests/conftest.py,pytest 会自动加载,测试文件不用导入。

# tests/conftest.py
import pytest
from playwright.sync_api import Page
from pages.search import SearchPage

@pytest.fixture
def search_page(page: Page) -> SearchPage:
    return SearchPage(page)

测试里直接把夹具名写进参数就能用:

def test_search(search_page: SearchPage) -> None:
    search_page.load()
    search_page.search("panda")

想改上下文配置(视口、语言、设备模拟),覆盖插件提供的 browser_context_args 夹具:

# tests/conftest.py
import pytest

@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
    return {
        **browser_context_args,
        "locale": "zh-CN",
        "viewport": {"width": 1440, "height": 900},
    }

注意 **browser_context_args 这一行。它先把插件原有配置展开,再覆盖自己关心的项,避免把默认值冲掉。

Page Object 的 Python 写法

Page Object(页面对象)的思路跟 TS 版一致:把定位器和交互封装进类。差别在语法。

# pages/search.py
from playwright.sync_api import Page

class SearchPage:

    URL = "https://www.duckduckgo.com"

    def __init__(self, page: Page) -> None:
        self.page = page
        self.search_input = page.locator("#search_form_input_homepage")
        self.search_button = page.locator("#search_button_homepage")

    def load(self) -> None:
        self.page.goto(self.URL)

    def search(self, phrase: str) -> None:
        self.search_input.fill(phrase)
        self.search_button.click()

三个要点:

  1. __init__ 相当于构造函数,把 page 注入进来存成实例变量。
  2. 定位器在 __init__ 里创建一次,全类复用。定位器是惰性的,创建时不碰页面。
  3. 页面对象里不写断言。它只负责取值和交互,判断留给测试函数。

页面对象类放 tests 目录外面(比如 pages/),并加一个空的 __init__.py,测试才能导入它。

命令行常用参数

pytest-playwright 把常用能力做成了命令行开关,不用改代码。

python3 -m pytest tests --browser firefox        # 换浏览器
python3 -m pytest tests --browser chromium --browser webkit  # 多浏览器跑
python3 -m pytest tests --headed --slowmo 500    # 有头 + 慢放
python3 -m pytest tests --device "iPhone 11"     # 设备模拟
python3 -m pytest tests --screenshot only-on-failure  # 失败才截图
python3 -m pytest tests --video retain-on-failure     # 失败才留视频
python3 -m pytest tests --tracing retain-on-failure   # 失败才留 trace

截图和视频别开 on。测试一多,产物体积会失控,only-on-failure 才是常态配置。

并行没有内置,装 pytest-xdist 补上:

pip install pytest-xdist
python3 -m pytest tests -n 4

并发度一般取 CPU 核数。上下文隔离是 Playwright 保证的,并行不会串数据。

小结

Python 版 = Playwright 库 + pytest + pytest-playwright 插件,三者分工明确。

同步 API 写测试,异步 API 留给爬虫类场景,别混用。

方法名蛇形化、选项变关键字参数,是翻译 TS 文档时最常改的两处。

expect 自动重试,裸 assert 不会,页面相关的断言一律走 expect

夹具用 conftest.py 组织,覆盖 browser_context_args 改上下文配置。