跳到主要内容

模拟 API

介绍​

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

模拟 API 请求​

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

test("模拟水果数据且不调用真实API", async ({ page }) => {
// 在导航前模拟 API 调用
await page.route('*/**/api/v1/fruits', async route => {
const json = [{ name: 'Strawberry', id: 21 }];
await route.fulfill({ json });
});
// 访问页面
await page.goto('https://demo.playwright.dev/api-mocking');

// 断言草莓水果可见
await expect(page.getByText('Strawberry')).toBeVisible();
});

从示例测试的跟踪记录中可以看到,API 从未被实际调用,而是用模拟数据进行了响应。api 模拟跟踪

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

修改 API 响应​

有时我们需要发起 API 请求,但为了确保测试可重复性,必须对响应进行修改。这种情况下,与其完全模拟请求,不如实际发起请求并使用修改后的响应来完成。

以下示例中,我们拦截了对水果 API 的调用,并在数据中添加了一个名为"枇杷"的新水果。然后我们访问页面并断言该数据存在:

test('从API获取JSON并添加新水果', async ({ page }) => {
// 获取响应并修改
await page.route('*/**/api/v1/fruits', async route => {
const response = await route.fetch();
const json = await response.json();
json.push({ name: 'Loquat', id: 100 });
// 使用原始响应完成请求,同时用给定的JSON对象修补响应体
await route.fulfill({ response, json });
});

// 访问页面
await page.goto('https://demo.playwright.dev/api-mocking');

// 断言新水果可见
await expect(page.getByText('Loquat', { exact: true })).toBeVisible();
});

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

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

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

使用 HAR 文件进行模拟​

HAR 文件是一种 HTTP 存档文件,记录了页面加载时发起的所有网络请求。它包含请求和响应头信息、cookies、内容、时间等数据。您可以使用 HAR 文件在测试中模拟网络请求。具体需要以下步骤:

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

录制 HAR 文件​

要录制 HAR 文件,我们使用 page.routeFromHAR() 或 browserContext.routeFromHAR() 方法。该方法接收 HAR 文件路径和一个可选配置对象。配置对象可以包含 URL 参数,这样只有匹配指定 glob 模式的请求才会从 HAR 文件中获取响应。如果不指定,所有请求都将从 HAR 文件中获取响应。

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

test('录制或更新 HAR 文件', async ({ page }) => {
// 从 HAR 文件获取响应
await page.routeFromHAR('./hars/fruit.har', {
url: '*/**/api/v1/fruits',
update: true,
});

// 访问页面
await page.goto('https://demo.playwright.dev/api-mocking');

// 断言水果可见
await expect(page.getByText('Strawberry')).toBeVisible();
});

修改 HAR 文件​

录制完成 HAR 文件后,您可以通过编辑 'hars' 文件夹中带哈希值的 .txt 文件来修改其中的 JSON 内容。该文件应当提交到您的源代码管理中。当您使用 update: true 参数运行此测试时,系统会用 API 的最新请求更新您的 HAR 文件。

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

从 HAR 文件回放​

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

test('从HAR获取json并检查新水果是否已添加', async ({ page }) => {
// 从HAR回放API请求
// 要么使用HAR中的匹配响应
// 如果没有匹配项则中止请求
await page.routeFromHAR('./hars/fruit.har', {
url: '*/**/api/v1/fruits',
update: false,
});

// 访问页面
await page.goto('https://demo.playwright.dev/api-mocking');

// 断言Playwright水果可见
await expect(page.getByText('Playwright', { exact: true })).toBeVisible();
});

在测试的追踪记录中,我们可以看到路由是通过 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" 归档文件
npx playwright open --save-har=example.har --save-har-glob="**/api/**" https://example.com

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

模拟 WebSocket​

以下代码将拦截 WebSocket 连接并模拟整个 WebSocket 通信,而不是连接到实际服务器。这个示例会响应 "request" 消息并返回 "response"。

await page.routeWebSocket('wss://example.com/ws', ws => {
ws.onMessage(message => {
if (message === 'request')
ws.send('response');
});
});

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

await page.routeWebSocket('wss://example.com/ws', ws => {
const server = ws.connectToServer();
ws.onMessage(message => {
if (message === 'request')
server.send('request2');
else
server.send(message);
});
});

更多细节请参阅 WebSocketRoute。