跳到主要内容

模拟 API

简介​

Web API 通常作为 HTTP 端点来实现。Playwright 提供了用于模拟和修改网络流量(包括 HTTP 和 HTTPS)的 API。页面发出的任何请求,包括 XHR 和 fetch 请求,都可以被跟踪、修改和模拟。借助 Playwright,你还可以使用包含页面发出的多个网络请求的 HAR 文件进行模拟。

模拟 API 请求​

以下代码将拦截对 */**/api/v1/fruits 的所有调用,并返回自定义响应。不会向 API 发出任何请求。测试将访问使用模拟路由的 URL,并断言页面上存在模拟数据。

// 拦截到水果 API 的路由
page.route("https://fruit.ceo/api/breeds/image/random", route -> {
List<Dictionary<String, Object>> data = new ArrayList<Dictionary<String, Object>>();
Hashtable<String, Object> dict = new Hashtable<String, Object>();
dict.put("name", "Strawberry");
dict.put("id", 21);
data.add(dict);
// 使用模拟数据来响应路由
route.fulfill(RequestOptions.create().setData(data));
});

// 访问页面
page.navigate("https://demo.playwright.dev/api-mocking");

// 断言草莓水果可见
assertThat(page.getByText("Strawberry")).isVisible();

从示例测试的跟踪信息中可以看到,API 从未被调用,而是使用模拟数据进行了响应。api mocking trace

了解更多关于 高级网络功能 的信息。

修改 API 响应​

有时,进行 API 请求至关重要,但响应需要进行修补,以便进行可重现的测试。在这种情况下,不必模拟请求,而是可以执行请求,并使用修改后的响应来完成请求。

在下面的示例中,我们拦截对水果 API 的调用,并向数据中添加一种名为 “枇杷” 的新水果。然后,我们访问该 URL,并断言数据已存在:

page.route("*/**/api/v1/fruits", route -> {
Response response = route.fetch();
byte[] json = response.body();
JsonObject parsed = new Gson().fromJson(new String(json), JsonObject.class);
parsed.add(new JsonObject().add("name", "Loquat").add("id", 100));
// 使用原始响应完成请求,同时使用给定的 JSON 对象修补响应体。
route.fulfill(new Route.FulfillOptions().setResponse(response).setBody(parsed.toString()));
});

// 导航到页面
page.navigate("https://demo.playwright.dev/api-mocking");

// 断言 “枇杷” 水果可见
assertThat(page.getByText("Loquat", new Page.GetByTextOptions().setExact(true))).isVisible();

在测试的跟踪记录中,我们可以看到 API 被调用且响应被修改。显示 API 被调用并完成的测试跟踪记录

通过检查响应,我们可以看到新水果已添加到列表中。显示模拟响应的测试跟踪记录

了解更多关于 高级网络 的内容。

使用 HAR 文件进行模拟​

HAR 文件是一种 HTTP 存档 文件,其中包含页面加载时发出的所有网络请求记录。它包含有关请求和响应标头、Cookie、内容、时间等信息。你可以在测试中使用 HAR 文件来模拟网络请求。你需要:

  1. 记录一个 HAR 文件。
  2. 将 HAR 文件与测试一起提交。
  3. 在测试中使用保存的 HAR 文件来路由请求。

记录 HAR 文件​

要记录 HAR 文件,我们使用 Page.routeFromHAR() 或 BrowserContext.routeFromHAR() 方法。此方法接受 HAR 文件的路径以及一个可选的选项对象。选项对象可以包含 URL,这样只有 URL 与指定通配符模式匹配的请求才会从 HAR 文件提供服务。如果未指定,则所有请求都将从 HAR 文件提供服务。

将 update 选项设置为 true 将使用实际网络信息创建或更新 HAR 文件,而不是从 HAR 文件提供请求服务。在创建测试以使用真实数据填充 HAR 时使用此选项。

// 从 HAR 文件获取响应
page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions()
.setUrl("*/**/api/v1/fruits")
.setUpdate(true)
);

// 导航到页面
page.navigate("https://demo.playwright.dev/api-mocking");

// 断言水果可见
assertThat(page.getByText("Strawberry")).isVisible();

修改 HAR 文件​

记录 HAR 文件后,你可以通过打开“hars”文件夹内经过哈希处理的 .txt 文件并编辑 JSON 来修改它。此文件应提交到你的源代码控制中。每次使用 update: true 运行此测试时,它都会使用来自 API 的请求更新你的 HAR 文件。

[
{
"name": "Playwright",
"id": 100
},
// ... 其他水果
]

从 HAR 重放​

现在你已经录制了 HAR 文件并修改了模拟数据,可以在测试中使用它来提供匹配的响应。为此,只需关闭或直接移除 update 选项。这样就会针对 HAR 文件运行测试,而不是访问 API。

// 从 HAR 重放 API 请求。
// 要么使用 HAR 中匹配的响应,
// 要么在无匹配项时中止请求。
page.routeFromHAR(Path.of("./hars/fruit.har"), new RouteFromHAROptions()
.setUrl("*/**/api/v1/fruits")
.setUpdate(false)
);

// 导航到页面
page.navigate("https://demo.playwright.dev/api-mocking");

// 断言 Playwright 水果可见
assertThat(page.getByText("Playwright", new Page.GetByTextOptions()
.setExact(true))).isVisible();

在测试的追踪信息中,我们可以看到路由是从 HAR 文件中获取的,并未调用 API。显示使用 HAR 文件的追踪信息

如果检查响应,我们可以看到新水果已添加到 JSON 中,这是通过手动更新 hars 文件夹内经过哈希处理的 .txt 文件完成的。显示来自 HAR 文件响应的追踪信息

HAR 重放会严格匹配 URL 和 HTTP 方法。对于 POST 请求,它还会严格匹配 POST 负载。如果有多个记录与请求匹配,则会选择匹配头信息最多的那一条。导致重定向的记录项会自动跟进。

与录制时类似,如果给定的 HAR 文件名以 .zip 结尾,则会将其视为一个存档,其中包含 HAR 文件以及作为单独条目存储的网络负载。你也可以解压此存档,手动编辑负载或 HAR 日志,并指向解压后的 HAR 文件。所有负载将相对于文件系统中解压后的 HAR 文件进行解析。

使用命令行界面(CLI)录制 HAR​

我们推荐使用 update 选项为测试录制 HAR 文件。不过,你也可以使用 Playwright CLI 录制 HAR。

使用 Playwright CLI 打开浏览器,并传递 --save-har 选项以生成一个 HAR 文件。你还可以选择使用 --save-har-glob 选项,仅保存你感兴趣的请求,例如 API 端点。如果 HAR 文件名以 .zip 结尾,相关文件将被分别写入,并全部压缩到一个 zip 文件中。

# 将来自 example.com 的 API 请求保存为 "example.har" 存档。
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="open --save-har=example.har --save-har-glob='**/api/**' https://example.com"

了解更多关于 高级网络功能 的信息。

模拟 WebSocket​

以下代码将拦截 WebSocket 连接,并模拟通过 WebSocket 的整个通信过程,而不是连接到服务器。此示例用 "response" 响应 "request"。

page.routeWebSocket("wss://example.com/ws", ws -> {
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
ws.send("response");
});
});

或者,你可能希望连接到实际服务器,但在消息传输过程中拦截并修改或阻止它们。以下示例修改了页面发送到服务器的部分消息,其余消息保持不变。

page.routeWebSocket("wss://example.com/ws", ws -> {
WebSocketRoute server = ws.connectToServer();
ws.onMessage(frame -> {
if ("request".equals(frame.text()))
server.send("request2");
else
server.send(frame.text());
});
});

更多详细信息,请参阅 WebSocketRoute。