首页
/ Playwright Java API 测试实战:用 APIRequestContext 打通 REST 接口测试与状态管理

Playwright Java API 测试实战:用 APIRequestContext 打通 REST 接口测试与状态管理

2026-09-06 18:45:49作者:余洋婵Anita

Playwright 不仅能驱动 Chromium、Firefox 和 WebKit 完成浏览器端 UI 自动化,还通过 APIRequestContext 提供了完整的 HTTP(S) API 测试能力,让 Java 开发者无需加载页面、无需在页面中执行 JS,即可直接向服务端发送请求。本文以 GitHub REST API 为实例,完整讲解如何在 JUnit 5 中用 Java 编写接口测试、预置服务端状态、校验服务端后置条件以及跨 BrowserContext 复用登录态,帮助你构建“API + UI”一体化的端到端测试体系。

一、为什么需要纯 API 层的测试能力

Playwright 定位为 Web 测试与自动化框架,其文档(docs/src/api-testing-java.md)开篇即指出:你可以用它访问应用暴露的 REST API。在某些场景下,我们希望在 Java 中直接向服务器发送请求,而不加载页面、不运行页面里的 JS 代码。官方文档归纳了三个典型动机:

  • 测试你的服务端 API:对接口本身的正确性做回归验证。
  • 访问 Web 应用之前预置服务端状态:例如先通过 API 创建账号、数据、仓库,再进入 UI 做交互断言,避免把“造数据”过程写进 UI 用例里。
  • 在浏览器中执行操作后,校验服务端后置条件:UI 上的点击最终是否真实写入了后端,用 API 请求做权威校验。

这三类需求都可以通过 APIRequestContext 的方法统一完成——它能发送所有种类的 HTTP(S) 请求。

1.1 APIRequestContext 在仓库中的实现位置

packages/playwright-core/src/client/fetch.ts 中可以找到 APIRequestContext 的完整协议实现:它同时提供 fetchgetpostputpatchdeletehead 等便捷方法,例如:

  • post(url, options)fetch.ts)以 POST 语义转发到统一的 fetch
  • get(url, options)fetch.ts)、delete(url, options)fetch.ts)同理;
  • 所有方法最终汇聚到 _innerFetch,在那里统一处理 paramsheadersdataformmultipart 等请求选项,并做合法性校验,例如只允许 dataformmultipart 三者中指定一个(见 fetch.ts)。

服务端的请求执行逻辑在 packages/playwright-core/src/server/fetch.ts:它会将 baseURL 与相对路径拼接(fetch.ts)、合并 extraHTTPHeadersfetch.ts),未显式指定 method 时默认使用 GETfetch.ts),重定向上限默认为 20 次。这些行为是 Java 绑定与仓库协议层共同遵循的契约。

二、编写 GitHub API 测试:从零开始的一个完整例子

官方文档用“通过 GitHub API 测试 Issue 创建”作为贯穿始终的实战示例,测试套件将依次完成:

  1. 运行测试前创建一个新仓库
  2. 创建若干个 Issue 并校验服务端状态
  3. 运行结束后删除该仓库

下面按「配置 → 用例 → 环境搭建与清理 → 完整代码」逐步展开。

2.1 配置:一次授权,全程复用(Configure)

GitHub API 需要鉴权。文档推荐的做法是在创建 APIRequestContext 时统一配置 token 与 baseURL,这样后续每个请求都不用重复携带鉴权头:

package org.example;

import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.APIRequestContext;
import com.microsoft.playwright.Playwright;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInstance;

import java.util.HashMap;
import java.util.Map;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestGitHubAPI {
  private static final String API_TOKEN = System.getenv("GITHUB_API_TOKEN");

  private Playwright playwright;
  private APIRequestContext request;

  void createPlaywright() {
    playwright = Playwright.create();
  }

  void createAPIRequestContext() {
    Map<String, String> headers = new HashMap<>();
    // We set this header per GitHub guidelines.
    headers.put("Accept", "application/vnd.github.v3+json");
    // Add authorization token to all requests.
    // Assuming personal access token available in the environment.
    headers.put("Authorization", "token " + API_TOKEN);

    request = playwright.request().newContext(new APIRequest.NewContextOptions()
      // All requests we send go to this API endpoint.
      .setBaseURL("https://api.github.com")
      .setExtraHTTPHeaders(headers));
  }

  @BeforeAll
  void beforeAll() {
    createPlaywright();
    createAPIRequestContext();
  }

  void disposeAPIRequestContext() {
    if (request != null) {
      request.dispose();
      request = null;
    }
  }

  void closePlaywright() {
    if (playwright != null) {
      playwright.close();
      playwright = null;
    }
  }

  @AfterAll
  void afterAll() {
    disposeAPIRequestContext();
    closePlaywright();
  }
}

这里值得注意的配置点有三个:

  • setBaseURL("https://api.github.com"):所有相对路径请求(如 /user/repos)都会拼接到该域名上。仓库协议实现中,拼接逻辑位于服务端 fetch 的 constructURLBasedOnBaseURL(defaults.baseURL, params.url)(见 packages/playwright-core/src/server/fetch.ts)。
  • setExtraHTTPHeaders(headers):为所有请求统一附加自定义头,鉴权信息只需配置一次。服务端实现会先把默认的 user-agentacceptaccept-encoding 头写入,再合并 extraHTTPHeaders 与单次请求头(见 fetch.ts)。
  • 生命周期管理@BeforeAll 创建 PlaywrightAPIRequestContext@AfterAll 依次 dispose() 掉请求上下文并 close() 掉 Playwright 驱动,保证测试进程干净退出。

API_TOKEN 与下文要用的 GITHUB_USER 都从环境变量读取,token 不硬编码进源码。运行测试前需要导出:

export GITHUB_USER="你的 GitHub 用户名"
export GITHUB_API_TOKEN="你的 GitHub Personal Access Token"

注意:仓库中与本文等价的开箱即用示例是 TypeScript 版本的 examples/github-api/tests/test-api.spec.ts(连同其 playwright.config.ts),它同样实现了“建仓 → 建 Issue → 删仓”流程,可对照学习跨语言写法。

2.2 编写测试:创建 Bug 报告与 Feature 请求

初始化好 request 对象后,就可以写真正发起接口调用的测试了。下面的用例分别创建 [Bug] report 1[Feature] request 1 两个 Issue,然后回读 Issue 列表验证服务端确实写入了对应标题与正文:

package org.example;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.APIRequestContext;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.RequestOptions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestGitHubAPI {
  private static final String REPO = "test-repo-2";
  private static final String USER = System.getenv("GITHUB_USER");
  private static final String API_TOKEN = System.getenv("GITHUB_API_TOKEN");

  private Playwright playwright;
  private APIRequestContext request;

  // ...

  @Test
  void shouldCreateBugReport() {
    Map<String, String> data = new HashMap<>();
    data.put("title", "[Bug] report 1");
    data.put("body", "Bug description");
    APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
      RequestOptions.create().setData(data));
    assertTrue(newIssue.ok());

    APIResponse issues = request.get("/repos/" + USER + "/" + REPO + "/issues");
    assertTrue(issues.ok());
    JsonArray json = new Gson().fromJson(issues.text(), JsonArray.class);
    JsonObject issue = null;
    for (JsonElement item : json) {
      JsonObject itemObj = item.getAsJsonObject();
      if (!itemObj.has("title")) {
        continue;
      }
      if ("[Bug] report 1".equals(itemObj.get("title").getAsString())) {
        issue = itemObj;
        break;
      }
    }
    assertNotNull(issue);
    assertEquals("Bug description", issue.get("body").getAsString(), issue.toString());
  }

  @Test
  void shouldCreateFeatureRequest() {
    Map<String, String> data = new HashMap<>();
    data.put("title", "[Feature] request 1");
    data.put("body", "Feature description");
    APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
      RequestOptions.create().setData(data));
    assertTrue(newIssue.ok());

    APIResponse issues = request.get("/repos/" + USER + "/" + REPO + "/issues");
    assertTrue(issues.ok());
    JsonArray json = new Gson().fromJson(issues.text(), JsonArray.class);
    JsonObject issue = null;
    for (JsonElement item : json) {
      JsonObject itemObj = item.getAsJsonObject();
      if (!itemObj.has("title")) {
        continue;
      }
      if ("[Feature] request 1".equals(itemObj.get("title").getAsString())) {
        issue = itemObj;
        break;
      }
    }
    assertNotNull(issue);
    assertEquals("Feature description", issue.get("body").getAsString(), issue.toString());
  }
}

上述代码演示了三个最常用的模式:

  1. 发送带 JSON body 的 POST:通过 RequestOptions.create().setData(map) 传入一个 Map,Playwright 客户端会把它序列化为 JSON 请求体(底层对应 packages/playwright-core/src/client/fetch.ts 中对 data 类型对象的 JSON.stringify 处理,见 fetch.ts)。
  2. 快速断言 HTTP 状态APIResponse.ok() 判断状态码是否落在 200–299 区间(仓库实现见 client/fetch.ts)。
  3. 解析响应文本为 JSONAPIResponse.text() 返回以 UTF-8 解码的响应体字符串(client/fetch.ts),再用 Gson 解析为 JsonArray 遍历查找目标记录,最后用 JUnit 的 assertNotNull / assertEquals 完成断言。

2.3 环境搭建与清理(Setup and teardown)

上面两个用例默认仓库已经存在。更规范的做法是在 @BeforeAll 里新建仓库、在 @AfterAll 里删掉仓库,保证测试环境幂等、可重复运行:

public class TestGitHubAPI {
  // ...

  void createTestRepository() {
    APIResponse newRepo = request.post("/user/repos",
      RequestOptions.create().setData(Collections.singletonMap("name", REPO)));
    assertTrue(newRepo.ok(), newRepo.text());
  }

  @BeforeAll
  void beforeAll() {
    createPlaywright();
    createAPIRequestContext();
    createTestRepository();
  }

  void deleteTestRepository() {
    if (request != null) {
      APIResponse deletedRepo = request.delete("/repos/" + USER + "/" + REPO);
      assertTrue(deletedRepo.ok());
    }
  }
  // ...

  @AfterAll
  void afterAll() {
    deleteTestRepository();
    disposeAPIRequestContext();
    closePlaywright();
  }
}

这里同样用到了几个值得解释的 API:

  • assertTrue(newRepo.ok(), newRepo.text()):第二个参数把失败时的响应体作为断言消息,排查 4xx/5xx 时能直接看到 GitHub 返回的错误 JSON,而不是干巴巴的布尔断言。
  • request.delete(...):DELETE 请求同样是 APIRequestContext 的一等公民方法。

2.4 完整测试示例(Complete test example)

把上述片段整合起来,就是一个可直接运行的完整 JUnit 测试类:

package org.example;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequest;
import com.microsoft.playwright.APIRequestContext;
import com.microsoft.playwright.APIResponse;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.options.RequestOptions;
import org.junit.jupiter.api.*;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestGitHubAPI {
  private static final String REPO = "test-repo-2";
  private static final String USER = System.getenv("GITHUB_USER");
  private static final String API_TOKEN = System.getenv("GITHUB_API_TOKEN");

  private Playwright playwright;
  private APIRequestContext request;

  void createPlaywright() {
    playwright = Playwright.create();
  }

  void createAPIRequestContext() {
    Map<String, String> headers = new HashMap<>();
    // We set this header per GitHub guidelines.
    headers.put("Accept", "application/vnd.github.v3+json");
    // Add authorization token to all requests.
    // Assuming personal access token available in the environment.
    headers.put("Authorization", "token " + API_TOKEN);

    request = playwright.request().newContext(new APIRequest.NewContextOptions()
      // All requests we send go to this API endpoint.
      .setBaseURL("https://api.github.com")
      .setExtraHTTPHeaders(headers));
  }

  void createTestRepository() {
    APIResponse newRepo = request.post("/user/repos",
      RequestOptions.create().setData(Collections.singletonMap("name", REPO)));
    assertTrue(newRepo.ok(), newRepo.text());
  }

  @BeforeAll
  void beforeAll() {
    createPlaywright();
    createAPIRequestContext();
    createTestRepository();
  }

  void deleteTestRepository() {
    if (request != null) {
      APIResponse deletedRepo = request.delete("/repos/" + USER + "/" + REPO);
      assertTrue(deletedRepo.ok());
    }
  }

  void disposeAPIRequestContext() {
    if (request != null) {
      request.dispose();
      request = null;
    }
  }

  void closePlaywright() {
    if (playwright != null) {
      playwright.close();
      playwright = null;
    }
  }

  @AfterAll
  void afterAll() {
    deleteTestRepository();
    disposeAPIRequestContext();
    closePlaywright();
  }

  @Test
  void shouldCreateBugReport() {
    Map<String, String> data = new HashMap<>();
    data.put("title", "[Bug] report 1");
    data.put("body", "Bug description");
    APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
      RequestOptions.create().setData(data));
    assertTrue(newIssue.ok());

    APIResponse issues = request.get("/repos/" + USER + "/" + REPO + "/issues");
    assertTrue(issues.ok());
    JsonArray json = new Gson().fromJson(issues.text(), JsonArray.class);
    JsonObject issue = null;
    for (JsonElement item : json) {
      JsonObject itemObj = item.getAsJsonObject();
      if (!itemObj.has("title")) {
        continue;
      }
      if ("[Bug] report 1".equals(itemObj.get("title").getAsString())) {
        issue = itemObj;
        break;
      }
    }
    assertNotNull(issue);
    assertEquals("Bug description", issue.get("body").getAsString(), issue.toString());
  }

  @Test
  void shouldCreateFeatureRequest() {
    Map<String, String> data = new HashMap<>();
    data.put("title", "[Feature] request 1");
    data.put("body", "Feature description");
    APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
      RequestOptions.create().setData(data));
    assertTrue(newIssue.ok());

    APIResponse issues = request.get("/repos/" + USER + "/" + REPO + "/issues");
    assertTrue(issues.ok());
    JsonArray json = new Gson().fromJson(issues.text(), JsonArray.class);
    JsonObject issue = null;
    for (JsonElement item : json) {
      JsonObject itemObj = item.getAsJsonObject();
      if (!itemObj.has("title")) {
        continue;
      }
      if ("[Feature] request 1".equals(itemObj.get("title").getAsString())) {
        issue = itemObj;
        break;
      }
    }
    assertNotNull(issue);
    assertEquals("Feature description", issue.get("body").getAsString(), issue.toString());
  }
}

文档同时提到:如果不想手工管理 Playwright 对象的创建与销毁,可以参考实验性的 JUnit 集成指南(Java),用其提供的 @UsePlaywright 注解与 request 等 fixture 自动初始化 APIRequestContext,把生命周期从测试代码里彻底剥离。

三、三种典型应用模式:API 与 UI 的协作

接口测试能力最大的价值在于和 UI 测试组合使用。官方文档给出两种最常用的交叉模式,下面分别说明。

3.1 模式一:先通过 API 预置状态,再用 UI 断言(Prepare server state)

这个模式解决的是“UI 测试跑得快且稳定”的问题:创建数据这类耗时、易碎的操作交给 API,UI 层只负责验证呈现结果。

public class TestGitHubAPI {
  @Test
  void lastCreatedIssueShouldBeFirstInTheList() {
    Map<String, String> data = new HashMap<>();
    data.put("title", "[Feature] request 1");
    data.put("body", "Feature description");
    APIResponse newIssue = request.post("/repos/" + USER + "/" + REPO + "/issues",
      RequestOptions.create().setData(data));
    assertTrue(newIssue.ok());

    try (Browser browser = playwright.chromium().launch()) {
      Page page = browser.newPage();
      page.navigate("https://github.com/" + USER + "/" + REPO + "/issues");
      Locator firstIssue = page.locator("a[data-hovercard-type='issue']").first();
      assertThat(firstIssue).hasText("[Feature] request 1");
    }
  }
}

流程拆解:

  1. 通过 request.post(...) 创建 Issue,assertTrue(newIssue.ok()) 先确认服务端接受;
  2. 启动 Chromium 浏览器并导航到 Issue 列表页;
  3. page.locator(...) 取到列表中第一个 Issue,通过 LocatorAssertions 风格的 assertThat(firstIssue).hasText(...) 断言其文本——API 写入的数据必须出现在 UI 列表的顶部。

3.2 模式二:先在 UI 操作,再用 API 校验服务端状态

这个模式反过来使用:UI 是“因”,API 是验证“果”的手段。它适用于确认 UI 操作真的把数据持久化到了后端,而不是前端“假成功”。

public class TestGitHubAPI {
  @Test
  void lastCreatedIssueShouldBeOnTheServer() {
    try (Browser browser = playwright.chromium().launch()) {
      Page page = browser.newPage();
      page.navigate("https://github.com/" + USER + "/" + 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();
      String issueId = page.url().substring(page.url().lastIndexOf('/'));

      APIResponse newIssue = request.get("https://github.com/" + USER + "/" + REPO + "/issues/" + issueId);
      assertThat(newIssue).isOK();
      assertTrue(newIssue.text().contains("Bug report 1"));
    }
  }
}

流程拆解:

  1. 在浏览器里完成“新建 Issue”的全部 UI 操作;
  2. 提交后,从 page.url() 中截取新 Issue 的 ID;
  3. request.get(...) 请求对应的 Issue 详情地址,先断言 APIResponse 整体 OK(对应 Playwright 的响应级断言,见 docs/src/api/class-apiresponseassertions.md),再检查响应体文本确实包含刚才填写的标题。

这两种模式各司其职、互为验证闭环,正是官方在文档开头强调的“预置服务端状态 / 校验服务端后置条件”的具体落地形态。

四、复用鉴权状态:storageState 在 Browser 与 API 之间互通

Web 应用普遍采用 Cookie 或 Token 鉴权,登录后的状态以 cookies 形式存储。APIRequestContext.storageState() 方法可以取出已认证上下文中的存储状态,并用它创建新的上下文。

storageState 的关键特性是BrowserContextAPIRequestContext 之间完全互通:你可以通过 API 调用完成登录,然后把拿到的 Cookie 塞进新建的浏览器上下文,页面访问时即为已登录状态——省去 UI 登录步骤。官方文档给出的代码骨架如下:

APIRequestContext requestContext = playwright.request().newContext(
  new APIRequest.NewContextOptions().setHttpCredentials("user", "passwd"));
requestContext.get("https://api.example.com/login");
// Save storage state into a variable.
String state = requestContext.storageState();

// Create a new context with the saved storage state.
BrowserContext context = browser.newContext(new Browser.NewContextOptions().setStorageState(state));

要点解释:

  • setHttpCredentials("user", "passwd"):给 API 上下文配置 HTTP Basic 认证凭据,用于登录请求。
  • requestContext.storageState():返回包含 cookies(以及可选 localStorage/origins 等)的 JSON 字符串。在仓库实现中,该方法还可配合 path 参数直接把状态落盘(见 client/fetch.ts)。
  • browser.newContext(...).setStorageState(state):新建的 BrowserContext 会预置这些 Cookie,进入页面即已完成鉴权。
  • 反向同样成立:BrowserContext.storageState() 的结果也能用来创建 APIRequestContext。这意味着通过 UI 登录一次,即可在后续的纯 API 请求中复用同一身份,也可以在任意方向执行“API 登录 → 浏览器复用”或“浏览器登录 → API 复用”。

从仓库协议实现看,服务端在创建请求上下文时会把传入的 storageState 解析为 cookies 与 origins 写入对应存储(见 packages/playwright-core/src/server/fetch.ts),并向外提供等价的 storageState() 读取方法,这保证了两个上下文体系间状态可以无损互换。

补充说明:若使用 BrowserContext/Page 内置的 request 对象而非独立创建的上下文,API 请求与浏览器会话还会共享同一个 Cookie jar(详见 APIRequestContext 官方类参考);而 playwright.request().newContext(...) 创建的独立上下文则拥有自己隔离的 Cookie 存储。

五、进阶:APIRequestContext 的常用选项与响应对象

围绕官方文档示例,再补充几个直接影响实战效果的配置与 API,方便按需查阅(Java 端全部用法以 docs/src/api/class-apirequest.mddocs/src/api/class-apirequestcontext.md 为准)。

5.1 创建上下文时可配置的关键项(APIRequest.NewContextOptions

配置 作用 示例中的用法
baseURL 为相对 URL 提供前缀,配合文档约定应/ 结尾省略路径 https://api.github.com
extraHTTPHeaders 给所有请求附加统一请求头 Accept / Authorization
httpCredentials HTTP Basic 认证凭据 setHttpCredentials("user", "passwd")
storageState 预置 Cookie/存储状态,用于复用登录 见第四节
ignoreHTTPSErrors 是否忽略 HTTPS 证书错误 内部实现对应关闭 rejectUnauthorized(见 server/fetch.ts
timeout 上下文级默认请求超时

5.2 单次请求选项(RequestOptions)与响应读取

每次 get/post/put/patch/delete 还可传入单次选项:

  • setParams(map):附加查询字符串参数(内部实现见 client/fetch.ts);
  • setHeaders(map):单次请求头(会与上下文级 extraHTTPHeaders 合并);
  • setData(object/string):JSON 请求体;换成 form 则是 urlencoded 表单,multipart 支持文件上传(三者互斥,详见 client/fetch.ts);
  • setTimeout(...):单次超时;此外底层还支持 maxRedirects(默认 20)与 maxRetries 等选项(见 server/fetch.ts)。

拿到 APIResponse 后,最常用的读取与断言手段包括:

API 作用
ok() 状态码是否 200–299(实现见 client/fetch.ts
status() / statusText() 读取原始状态码与状态文本
url() 最终响应 URL(可判断重定向去向)
headers() / headersArray() 读取响应头
text() 以 UTF-8 解码响应体为字符串(client/fetch.ts
body() 以字节数组形式返回响应体(client/fetch.ts
响应级断言 assertThat(response).isOK() 语义化断言,见 docs/src/api/class-apiresponseassertions.md

六、小结

从本文可以看到,Playwright 的 API 测试能力并不是独立于 UI 自动化之外的“另一个框架”,而是与浏览器自动化深度咬合的一套完整闭环:

  • APIRequestContext 发送各类 HTTP(S) 请求,直接测试服务端接口;
  • 在 UI 测试前用它预置数据、在 UI 操作后用它对后端做权威校验;
  • 通过 storageState()BrowserContextAPIRequestContext 之间无缝迁移鉴权状态。

若希望进一步自动化管理 Playwright 对象的生命周期、利用 request fixture 免去样板代码,推荐阅读实验性的 JUnit 集成指南(Java);类级别的完整方法签名、参数与取值边界则以仓库中的 APIRequestContextAPIResponse 等官方 API 参考文档为准。仓库内还提供了 TypeScript 版本的等价落地示例 examples/github-api,可作为多语言对照或 CI 冒烟测试的起点。

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