首页
/ Playwright Python API 测试实战:用 APIRequestContext 测试 REST 接口与验证服务端状态

Playwright Python API 测试实战:用 APIRequestContext 测试 REST 接口与验证服务端状态

2026-09-05 11:43:27作者:姚月梅Lane

本文基于 Playwright 官方文档 API testing (Python) 编写,围绕 APIRequestContext 这一核心对象展开:如何在纯 Python 环境下不启动页面直接发送 HTTP(S) 请求测试 REST API、如何用 API 调用为浏览器测试预置服务端状态、如何在用户操作后通过 API 校验服务端落库结果,以及如何借助 storage_stateBrowserContextAPIRequestContext 之间复用认证状态。读完本文,你可以用 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.requestpage.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(单次请求级覆盖)、timeoutfail_on_status_codeignore_https_errorsmax_redirectsmax_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_TOKENGITHUB_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 下的 retriesworkers 等常规配置),可作为跨语言参考。

用 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 请求校验服务端存储的 titlebody。这里演示了"UI 操作 → 服务端后置条件校验"的完整闭环——仅靠 UI 断言无法确认数据真正持久化,而 API 校验恰好补足这一层。

复用认证状态:storage_state 在两类上下文间互换

Web 应用普遍使用基于 Cookie 或 token 的认证,认证状态以 Cookie 形式保存。Playwright 提供 APIRequestContext.storage_state 方法,可以从已认证的上下文中取出 storage state,再用它创建新的上下文。关键点:storage state 在 BrowserContextAPIRequestContext 之间是可互换的——你可以先用 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(含 namevaluedomainpathexpireshttpOnlysecuresameSite 字段)与 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 上下文之间双向流通。

可继续在仓库中查阅的资料:

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