跳到主要内容

Mock APIs (模拟 API)

简介

Web APIs 通常作为 HTTP 端点实现。Playwright 提供了模拟修改网络流量的 API,包括 HTTP 和 HTTPS。页面发起的任何请求,包括 XHRsfetch 请求,都可以被追踪、修改和模拟。使用 Playwright 还可以使用包含多个页面网络请求的 HAR 文件进行模拟。整个测试过程中不会调用真实 API。

模拟 API 请求

以下代码将拦截所有对 */**/api/v1/fruits 的调用,并返回自定义响应。没有请求会真正发送到 API。测试访问了使用此模拟路由的 URL,并断言页面上显示了模拟数据。

def test_mock_the_fruit_api(page: Page):
def handle(route: Route):
json = [{"name": "Strawberry", "id": 21}]
# fulfill the route with the mock data
route.fulfill(json=json)

# Intercept the route to the fruit API
page.route("*/**/api/v1/fruits", handle)

# Go to the page
page.goto("https://demo.playwright.dev/api-mocking")

# Assert that the Strawberry fruit is visible
expect(page.get_by_text("Strawberry")).to_be_visible()

你可以从示例测试的跟踪中看到,API 实际上从未被调用,它只是通过模拟数据完成了请求。api mocking trace

关于高级网络的更多信息请阅读更多相关内容。

修改 API 响应

有时候,执行一个 API 请求是必要的,但需要修改其响应以允许可重复的测试。在这种情况下,您可以不模拟请求,而是执行请求并使用修改后的响应来完成它。

在下面的示例中,我们拦截了对水果 API 的调用并向数据中添加了一个名为 'Loquat' 的新水果。然后我们前往该网址并断言这些数据存在:

def test_gets_the_json_from_api_and_adds_a_new_fruit(page: Page):
def handle(route: Route):
response = route.fetch()
json = response.json()
json.append({ "name": "Loquat", "id": 100})
# Fulfill using the original response, while patching the response body
# with the given JSON object.
route.fulfill(response=response, json=json)

page.route("https://demo.playwright.dev/api-mocking/api/v1/fruits", handle)

# Go to the page
page.goto("https://demo.playwright.dev/api-mocking")

# Assert that the new fruit is visible
expect(page.get_by_text("Loquat", exact=True)).to_be_visible()

在我们的测试跟踪中可以看到 API 被调用了并且响应也被修改了。trace of test showing api being called and fulfilled

通过检查响应我们可以看到我们的新水果已经被添加到了列表中。trace of test showing the mock response

关于高级网络的更多信息请阅读更多相关内容。

使用 HAR 文件进行模拟

HAR 文件是一个包含加载页面时所做网络请求记录的文件。它包含了请求和响应头、cookie、内容、时间等信息。你可以在测试中使用 HAR 文件来模拟网络请求。你需要:

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

记录 HAR 文件

要记录 HAR 文件,我们使用 page.route_from_har()browser_context.route_from_har() 方法。这个方法接受 HAR 文件路径和一个可选选项对象。选项对象可以包含 URL,以便只有符合指定 glob 模式的 URL 的请求才会从 HAR 文件中提供服务。如果没有指定,则所有请求都将从 HAR 文件中提供服务。

设置 update 选项为 true 将创建或更新 HAR 文件,并使用实际网络信息填充它。在创建测试时使用它,以便用真实数据填充 HAR。

def test_records_or_updates_the_har_file(page: Page):
# Get the response from the HAR file
page.route_from_har("./hars/fruit.har", url="*/**/api/v1/fruits", update=True)

# Go to the page
page.goto("https://demo.playwright.dev/api-mocking")

# Assert that the fruit is visible
expect(page.get_by_text("Strawberry")).to_be_visible()

修改 HAR 文件

一旦你记录了一个 HAR 文件,你可以通过打开你的 'hars' 文件夹中的哈希 .txt 文件并编辑 JSON 来修改它。这个文件应该提交到你的源控制系统中。每次运行这个测试时如果带有 update: true 它将会用 API 请求的结果更新你的 HAR 文件。

[
{
"name": "Playwright",
"id": 100
},
// ... other fruits
]

从 HAR 回放

现在你已经录制并修改了 HAR 文件,它可以用于在测试中提供匹配的响应。为此,只需关闭或删除 update 选项。这样,测试将针对 HAR 文件而不是调用 API 运行。

def test_gets_the_json_from_har_and_checks_the_new_fruit_has_been_added(page: Page):
# Replay API requests from HAR.
# Either use a matching response from the HAR,
# or abort the request if nothing matches.
page.route_from_har("./hars/fruit.har", url="*/**/api/v1/fruits", update=False)

# Go to the page
page.goto("https://demo.playwright.dev/api-mocking")

# Assert that the Playwright fruit is visible
expect(page.get_by_text("Playwright", exact=True)).to_be_visible()

在我们的测试跟踪中可以看到路由是从 HAR 文件完成的,并且没有调用 API。trace showing the HAR file being used

如果我们检查响应,我们可以看到我们的新水果已被添加到 JSON 中,这是通过手动更新 hars 文件夹内的哈希 .txt 文件完成的。trace showing response from HAR file

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

# Save API requests from example.com as "example.har" archive.
playwright open --save-har=example.har --save-har-glob="**/api/**" https://example.com

关于高级网络的更多信息请阅读更多相关内容。

模拟 WebSockets

以下代码将拦截 WebSocket 连接并模拟整个 WebSocket 上的通信,而不是连接到服务器。此示例会对 "request" 发送 "response" 做出响应。

def message_handler(ws: WebSocketRoute, message: Union[str, bytes]):
if message == "request":
ws.send("response")

page.route_web_socket("wss://example.com/ws", lambda ws: ws.on_message(
lambda message: message_handler(ws, message)
))

或者,您可能想要连接到实际服务器,但在中间拦截消息并修改或阻止它们。这里有一个示例,它修改了页面发送给服务器的一些消息,并让其余消息保持不变。

def message_handler(server: WebSocketRoute, message: Union[str, bytes]):
if message == "request":
server.send("request2")
else:
server.send(message)

def handler(ws: WebSocketRoute):
server = ws.connect_to_server()
ws.on_message(lambda message: message_handler(server, message))

page.route_web_socket("wss://example.com/ws", handler)

有关详细信息,请参见 WebSocketRoute