模拟 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 响应
有时,进行 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 被调用且响应被修改。
通过检查响应,我们可以看到新水果已添加到列表中。
了解更多关于 高级网络 的内容。
使用 HAR 文件进行模拟
HAR 文件是一种 HTTP 存档 文件,其中包含页面加载时发出的所有网络请求记录。它包含有关请求和响应标头、Cookie、内容、时间等信息。你可以在测试中使用 HAR 文件来模拟网络请求。你需要:
- 记录一个 HAR 文件。
- 将 HAR 文件与测试一起提交。
- 在测试中使用保存的 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。
如果检查响应,我们可以看到新水果已添加到 JSON 中,这是通过手动更新 hars
文件夹内经过哈希处理的 .txt
文件完成的。
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。