Python 版入门与差异
本教程共 59 篇 · 第 57 篇 · 更新于 2026-08-04 · 约 11 分钟阅读
本节目标:装好 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 会连带把 playwright 和 pytest 一起拉下来。
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 版里 page、context 是测试运行器内置的。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 def 加 await,函数还得用 asyncio.run 启动。
写端到端测试就用同步版。测试本来就是一步接一步,异步带来的并发收益几乎为零,反而多了一堆 await。
Warning
pytest-playwright提供的是同步夹具。想在 pytest 里写异步测试,要换成官方的pytest-playwright-asyncio插件,两者不要同时装。
命名与传参差异
Python 版把 API 翻译成了地道的 Python 写法,主要是两条规则。
方法名蛇形化:getByRole 变 get_by_role,allTextContents 变 all_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()
三个要点:
__init__相当于构造函数,把page注入进来存成实例变量。- 定位器在
__init__里创建一次,全类复用。定位器是惰性的,创建时不碰页面。 - 页面对象里不写断言。它只负责取值和交互,判断留给测试函数。
页面对象类放 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 改上下文配置。