跳到主要内容

Mock APIs (模拟 API)

简介​

Web APIs 通常作为 HTTP 端点实现。Playwright 提供了模拟和修改网络流量的 API,包括 HTTP 和 HTTPS。页面发起的任何请求,包括 XHRs 和 fetch 请求,都可以被追踪、修改和模拟。使用 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。