Playwright Python API 测试实战:用 APIRequestContext 测试 REST 接口与验证服务端状态
本文基于 Playwright 官方文档 API testing (Python) 编写,围绕 APIRequestContext 这一核心对象展开:如何在纯 Python 环境下不启动页面直接发送 HTTP(S) 请求测试 REST API、如何用 API 调用为浏览器测试预置服务端状态、如何在用户操作后通过 API 校验服务端落库结果,以及如何借助 storage_state 在 BrowserContext 与 APIRequestContext 之间复用认证状态。读完本文,你可以用 pytest-playwright 搭建一套可运行的 API 测试套件,并理解各配置项与响应对象在底层的行为机制。
三种典型使用场景
Playwright 除了驱动浏览器,还可以直接访问应用的 REST API。文档明确列出了三个典型场景:
- 直接测试服务端 API:不加载页面、不在其中执行 JS,纯粹验证接口行为;
- 访问 Web 应用前预置服务端状态:比如先通过 API 创建好测试数据,再打开页面;
- 浏览器操作完成后校验服务端后置条件:UI 操作结束后,用 API 确认数据确实写入服务端。
这三类需求均可通过 APIRequestContext 的方法实现。APIRequestContext 自 v1.16 起提供,支持发送各种类型的 HTTP(S) 请求(get/post/put/delete/patch/head/fetch),会自动跟随重定向、从上下文填充请求 Cookie、并用响应中的 Set-Cookie 更新上下文。
一个重要的机制区别值得先明确:每个 Playwright 浏览器上下文都关联一个 APIRequestContext(通过 browser_context.request 或 page.request 访问),它与 BrowserContext 共享同一个 Cookie 罐——通过 API 登录会连带登录浏览器;而通过 playwright.request.new_context() 创建的是独立的隔离实例,拥有自己独立的 Cookie 存储。本文的 API 测试正是采用后者的独立上下文方式。
配置:会话级 fixture 设置 baseURL 与鉴权头
以下示例以 GitHub API 为测试目标,测试套件整体要做三件事:测试前创建新仓库、创建若干 issue 并校验服务端状态、测试后删除仓库。
GitHub API 需要授权,因此文档建议用一个 session 级 fixture 一次性配置好 token 与 base_url,让后续所有测试的路径都只需写相对路径。这里依赖 pytest-playwright 提供的 playwright fixture(同步 API 版本):
import os
from typing import Generator
import pytest
from playwright.sync_api import Playwright, APIRequestContext
GITHUB_API_TOKEN = os.getenv("GITHUB_API_TOKEN")
assert GITHUB_API_TOKEN, "GITHUB_API_TOKEN is not set"
@pytest.fixture(scope="session")
def api_request_context(
playwright: Playwright,
) -> Generator[APIRequestContext, None, None]:
headers = {
# We set this header per GitHub guidelines.
"Accept": "application/vnd.github.v3+json",
# Add authorization token to all requests.
# Assuming personal access token available in the environment.
"Authorization": f"token {GITHUB_API_TOKEN}",
}
request_context = playwright.request.new_context(
base_url="https://api.github.com", extra_http_headers=headers
)
yield request_context
request_context.dispose()
参数说明(依据 APIRequestContext API 文档):
base_url:请求 URL 的基准前缀,测试中写/repos/...即可拼接出完整地址;extra_http_headers:附加到每一个请求上的公共头,Accept: application/vnd.github.v3+json是 GitHub API 的规范要求,Authorization: token <PAT>携带个人访问令牌;request_context.dispose():dispose会释放该上下文及其所有响应占用的内存,之后再调用其任何方法都会抛异常——响应体会缓存在内存中以便后续调用response.body(),因此显式 dispose 是良好的资源管理习惯。
仓库中还提供了一个 JS/TS 版本的同款示例 examples/github-api/tests/test-api.spec.ts,其结构与 Python 版本一一对应:test.use({ baseURL, extraHTTPHeaders }) 对应 new_context(base_url=..., extra_http_headers=...),beforeAll/afterAll 对应 pytest 的 session fixture。
编写 API 测试:创建 issue 并校验服务端状态
fixture 就绪后,测试体非常直接:post 发送 JSON 数据创建 issue,再 get 拉取 issue 列表做断言:
import os
from typing import Generator
import pytest
from playwright.sync_api import Playwright, APIRequestContext
GITHUB_API_TOKEN = os.getenv("GITHUB_API_TOKEN")
assert GITHUB_API_TOKEN, "GITHUB_API_TOKEN is not set"
GITHUB_USER = os.getenv("GITHUB_USER")
assert GITHUB_USER, "GITHUB_USER is not set"
GITHUB_REPO = "test"
# ...
def test_should_create_bug_report(api_request_context: APIRequestContext) -> None:
data = {
"title": "[Bug] report 1",
"body": "Bug description",
}
new_issue = api_request_context.post(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data)
assert new_issue.ok
issues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")
assert issues.ok
issues_response = issues.json()
issue = list(filter(lambda issue: issue["title"] == "[Bug] report 1", issues_response))[0]
assert issue
assert issue["body"] == "Bug description"
def test_should_create_feature_request(api_request_context: APIRequestContext) -> None:
data = {
"title": "[Feature] request 1",
"body": "Feature description",
}
new_issue = api_request_context.post(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data)
assert new_issue.ok
issues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")
assert issues.ok
issues_response = issues.json()
issue = list(filter(lambda issue: issue["title"] == "[Feature] request 1", issues_response))[0]
assert issue
assert issue["body"] == "Feature description"
这里涉及 APIResponse 对象的几个核心成员:
response.ok:布尔值,状态码在 200–299 范围内为True;response.json():将响应体解析为 Python 对象(同步 API 下可直接调用;异步 API 下需await);response.status/response.statusText:状态码与状态文本(如 200 / "OK");response.headers:响应头字典;response.text()、response.body():文本与原始字节体;response.url:响应最终 URL(跟随重定向后)。
post 方法的 data 参数直接接受 Python 字典,Playwright 会将其序列化为 JSON 请求体并自动处理 Content-Type;若需 application/x-www-form-urlencoded 表单则改用 form 参数,multipart 参数则用于 multipart/form-data 文件上传。此外每个请求方法还支持 params(URL 查询参数)、headers(单次请求级覆盖)、timeout、fail_on_status_code、ignore_https_errors、max_redirects、max_retries 等选项,详见 APIRequestContext 方法签名。
Setup 与 Teardown:会话级 autouse fixture
上述测试假设仓库已存在。合理的做法是用一个 session 级 fixture 在所有测试前创建仓库、测试结束后删除。fixture 中 yield 之前的部分是 "before all",之后是 "after all":
# ...
@pytest.fixture(scope="session", autouse=True)
def create_test_repository(
api_request_context: APIRequestContext,
) -> Generator[None, None, None]:
# Before all
new_repo = api_request_context.post("/user/repos", data={"name": GITHUB_REPO})
assert new_repo.ok
yield
# After all
deleted_repo = api_request_context.delete(f"/repos/{GITHUB_USER}/{GITHUB_REPO}")
assert deleted_repo.ok
autouse=True 使该 fixture 无需在测试函数参数中声明即可自动生效。注意两个 fixture 的依赖关系:create_test_repository 依赖 api_request_context,pytest 会保证后者先创建。
完整测试示例
将以上三部分合并,得到文档给出的完整可运行示例(前置环境变量:GITHUB_API_TOKEN 与 GITHUB_USER):
from enum import auto
import os
from typing import Generator
import pytest
from playwright.sync_api import Playwright, Page, APIRequestContext, expect
GITHUB_API_TOKEN = os.getenv("GITHUB_API_TOKEN")
assert GITHUB_API_TOKEN, "GITHUB_API_TOKEN is not set"
GITHUB_USER = os.getenv("GITHUB_USER")
assert GITHUB_USER, "GITHUB_USER is not set"
GITHUB_REPO = "test"
@pytest.fixture(scope="session")
def api_request_context(
playwright: Playwright,
) -> Generator[APIRequestContext, None, None]:
headers = {
# We set this header per GitHub guidelines.
"Accept": "application/vnd.github.v3+json",
# Add authorization token to all requests.
# Assuming personal access token available in the environment.
"Authorization": f"token {GITHUB_API_TOKEN}",
}
request_context = playwright.request.new_context(
base_url="https://api.github.com", extra_http_headers=headers
)
yield request_context
request_context.dispose()
@pytest.fixture(scope="session", autouse=True)
def create_test_repository(
api_request_context: APIRequestContext,
) -> Generator[None, None, None]:
# Before all
new_repo = api_request_context.post("/user/repos", data={"name": GITHUB_REPO})
assert new_repo.ok
yield
# After all
deleted_repo = api_request_context.delete(f"/repos/{GITHUB_USER}/{GITHUB_REPO}")
assert deleted_repo.ok
def test_should_create_bug_report(api_request_context: APIRequestContext) -> None:
data = {
"title": "[Bug] report 1",
"body": "Bug description",
}
new_issue = api_request_context.post(
f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data
)
assert new_issue.ok
issues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")
assert issues.ok
issues_response = issues.json()
issue = list(
filter(lambda issue: issue["title"] == "[Bug] report 1", issues_response)
)[0]
assert issue
assert issue["body"] == "Bug description"
def test_should_create_feature_request(api_request_context: APIRequestContext) -> None:
data = {
"title": "[Feature] request 1",
"body": "Feature description",
}
new_issue = api_request_context.post(
f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data
)
assert new_issue.ok
issues = api_request_context.get(f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues")
assert issues.ok
issues_response = issues.json()
issue = list(
filter(lambda issue: issue["title"] == "[Feature] request 1", issues_response)
)[0]
assert issue
assert issue["body"] == "Feature description"
该示例在仓库中的 TypeScript 对应版本见 examples/github-api(其 playwright.config.ts 展示了 forbidOnly、CI 下的 retries 与 workers 等常规配置),可作为跨语言参考。
用 API 预置服务端状态,再用 UI 断言
API 测试与浏览器测试可以无缝组合。下面的测试先通过 API 连续创建两个 issue,然后打开 issue 列表页,用 LocatorAssertions 断言最新创建的排在列表顶部:
def test_last_created_issue_should_be_first_in_the_list(api_request_context: APIRequestContext, page: Page) -> None:
def create_issue(title: str) -> None:
data = {
"title": title,
"body": "Feature description",
}
new_issue = api_request_context.post(
f"/repos/{GITHUB_USER}/{GITHUB_REPO}/issues", data=data
)
assert new_issue.ok
create_issue("[Feature] request 1")
create_issue("[Feature] request 2")
page.goto(f"https://github.com/{GITHUB_USER}/{GITHUB_REPO}/issues")
first_issue = page.locator("a[data-hovercard-type='issue']").first
expect(first_issue).to_have_text("[Feature] request 2")
要点在于两条链路的分工:api_request_context(独立上下文)负责快速、稳定地铺数据,page fixture 负责真实渲染验证。注意此处 API 上下文是隔离的,它的 Cookie 不会与 page 所在浏览器上下文互通——如果需要共享登录态,应改用 page.request 或传递 storage state(见下节)。
浏览器操作后,用 API 校验服务端落库
与上一步方向相反:先通过 UI 完成用户操作,再回查 API 确认服务端状态确实变化:
def test_last_created_issue_should_be_on_the_server(api_request_context: APIRequestContext, page: Page) -> None:
page.goto(f"https://github.com/{GITHUB_USER}/{GITHUB_REPO}/issues")
page.locator("text=New issue").click()
page.locator("[aria-label='Title']").fill("Bug report 1")
page.locator("[aria-label='Comment body']").fill("Bug description")
page.locator("text=Submit new issue").click()
issue_id = page.url.split("/")[-1]
new_issue = api_request_context.get(f"https://github.com/{GITHUB_USER}/{GITHUB_REPO}/issues/{issue_id}")
assert new_issue.ok
assert new_issue.json()["title"] == "[Bug] report 1"
assert new_issue.json()["body"] == "Bug description"
流程细节:提交后 GitHub 会导航到新建 issue 的详情页,测试从 page.url 中截取末段作为 issue_id,随后发起 GET 请求校验服务端存储的 title 与 body。这里演示了"UI 操作 → 服务端后置条件校验"的完整闭环——仅靠 UI 断言无法确认数据真正持久化,而 API 校验恰好补足这一层。
复用认证状态:storage_state 在两类上下文间互换
Web 应用普遍使用基于 Cookie 或 token 的认证,认证状态以 Cookie 形式保存。Playwright 提供 APIRequestContext.storage_state 方法,可以从已认证的上下文中取出 storage state,再用它创建新的上下文。关键点:storage state 在 BrowserContext 与 APIRequestContext 之间是可互换的——你可以先用 API 调用完成登录(免启动浏览器的快速认证),然后把带 Cookie 的状态注入新的浏览器上下文。
request_context = playwright.request.new_context(http_credentials={"username": "test", "password": "test"})
request_context.get("https://api.example.com/login")
# Save storage state into a variable.
state = request_context.storage_state()
# Create a new context with the saved storage state.
context = browser.new_context(storage_state=state)
storage_state() 返回一个包含 cookies(含 name、value、domain、path、expires、httpOnly、secure、sameSite 字段)与 origins(localStorage 快照)的对象;browser.new_context(storage_state=...) 则接收同样的结构。反向操作也成立:先经浏览器完成登录,再 browser_context.storage_state() 导出,供后续 APIRequestContext 使用,从而跳过重复的登录流程。
小结与延伸阅读
- 纯 API 测试:
playwright.request.new_context(base_url=..., extra_http_headers=...)创建独立上下文,session 级 fixture 管理生命周期,dispose()释放资源; - API + UI 组合:用 API 铺数据、用
expect(locator)断言 UI,或用 UI 操作后get回查服务端状态; - 认证复用:
storage_state在浏览器上下文与 API 上下文之间双向流通。
可继续在仓库中查阅的资料:
- 原始文档:docs/src/api-testing-python.md
- 同类文档(JS / C# 版本):docs/src/api-testing-js.md、docs/src/api-testing-csharp.md
- API 参考:APIRequestContext、APIResponse
- TS 版 GitHub API 示例:examples/github-api/tests/test-api.spec.ts
- Python 测试运行器(pytest-playwright fixture 说明):docs/src/running-tests-python.md
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00