ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Playwright API Mock 指南:拦截 HTTP/HTTPS 请求、HAR 回放与 WebSocket 仿真

Playwright API Mock 指南:拦截 HTTP/HTTPS 请求、HAR 回放与 WebSocket 仿真 Playwright API Mock 指南拦截 HTTP/HTTPS 请求、HAR 回放与 WebSocket 仿真【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright本篇技术指南基于 Playwright 官方文档 docs/src/mock.md 展开系统讲解如何利用 Playwright 提供的网络拦截能力对 HTTP/HTTPS 请求进行mock模拟与修改使用HARHTTP Archive文件实现录制—改档—回放的接口级仿真并进一步覆盖 WebSocket 的整链路模拟。阅读并动手实践后你将掌握 page.route / route.fulfill / routeFromHAR / routeWebSocket 等核心 API 的组合用法能够把前端测试中的外部接口依赖彻底解耦出来实现真正确定性的自动化测试。为什么需要在测试中 Mock APIWeb API 通常以 HTTP 端点形式对外暴露。Playwright 内置了一套网络层拦截机制可以对页面发出的所有 HTTP 与 HTTPS 流量进行跟踪、修改和 mock——包括浏览器内的 XHR 与 fetch 请求甚至在发出请求之前就将其拦下。这带来几个直接收益测试不依赖后端环境开发机、CI、预发是否可用、数据是否就绪接口返回慢、不稳定、需要特定条件数据时测试依然稳定可复现无需修改被测试的应用代码Mock 逻辑全部位于测试内部可模拟异常、超时、空数据等难以构造的真实场景。除了手工 mockPlaywright 还支持用HAR 文件批量回放页面加载时产生的多条网络请求记录让整页离线可测成为可能。基础篇拦截并替换 API 响应最直接的 Mock 方式是使用page.route()注册一个 URL 匹配规则当页面触发匹配请求时由测试代码接管该请求并调用route.fulfill()返回自定义响应。此时真实 API 请求根本不会发出。下面以拦截*/**/api/v1/fruits为例测试流程是先注册拦截规则再打开使用该接口的页面最后断言 mock 数据出现在页面上。// JavaScript / TypeScript (Playwright Test) test(mocks a fruit and doesnt call api, async ({ page }) { // Mock the api call before navigating await page.route(*/**/api/v1/fruits, async route { const json [{ name: Strawberry, id: 21 }]; await route.fulfill({ json }); }); // Go to the page await page.goto(https://demo.playwright.dev/api-mocking); // Assert that the Strawberry fruit is visible await expect(page.getByText(Strawberry)).toBeVisible(); });# Python (Async API) async def test_mock_the_fruit_api(page: Page): async def handle(route: Route): json [{name: Strawberry, id: 21}] # fulfill the route with the mock data await route.fulfill(jsonjson) # Intercept the route to the fruit API await page.route(*/**/api/v1/fruits, handle) # Go to the page await page.goto(https://demo.playwright.dev/api-mocking) # Assert that the Strawberry fruit is visible await expect(page.get_by_text(Strawberry)).to_be_visible()# Python (Sync API) 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(jsonjson) # 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()// C# (NUnit / xUnit) // Intercept the route to the fruit API await page.RouteAsync(*/**/api/v1/fruits, async route { var json new[] { new { name Strawberry, id 21 } }; // fulfill the route with the mock data await route.FulfillAsync(new() { Json json }); }); // Go to the page await page.GotoAsync(https://demo.playwright.dev/api-mocking); // Assert that the Strawberry fruit is visible await Expect(page.GetByText(Strawberry)).ToBeVisibleAsync();// Java (JUnit) // Intercept the route to the fruit API page.route(*/**/api/v1/fruits, route - { ListDictionaryString, Object data new ArrayListDictionaryString, Object(); HashtableString, Object dict new HashtableString, Object(); dict.put(name, Strawberry); dict.put(id, 21); data.add(dict); // fulfill the route with the mock data route.fulfill(RequestOptions.create().setData(data)); }); // Go to the page page.navigate(https://demo.playwright.dev/api-mocking); // Assert that the Strawberry fruit is visible assertThat(page.getByText(Strawberry)).isVisible();要点与底层实现先注册、后导航route必须在触发请求的goto之前完成注册否则拦截不生效。URL 匹配语法*/**/api/v1/fruits中的*匹配任意单个字符**匹配任意多字符可跨越/因此该写法同时覆盖了http(s)://前缀下的多种 host。匹配范围可细化page.route()的 URL 参数支持 glob 字符串、正则表达式或函数形式的谓词predicate可按需拦截一类接口。fulfill 的自动序列化传入json选项时客户端会自动JSON.stringify并设置content-type: application/json其实现位于 packages/playwright-core/src/client/network.ts#L373-L427Route._innerFulfill——从源码可以看到json、body、path三者的互斥约束与 content-type 推断逻辑。response 默认值当不显式传status时默认 200见_innerFulfill中statusOption || 200。从示例测试生成的trace跟踪记录中可以清楚看到该 URL 的请求从未真正到达 API 服务器而是直接由 mock 数据完成响应。结合源码理解 Route 生命周期Route对象代表一个被拦截的请求。客户端侧关键方法见 packages/playwright-core/src/client/network.ts#L303-L458route.fetch()以 APIRequestContext 名义真正发起一次网络请求拿到真实APIResponse后供改写route.fulfill({ response, ... })用既有 response 的状态码与响应头为基底替换 body 后再返回给页面route.abort()直接取消请求route.continue()/route.fallback()放行请求后者用于把处理权交给更早注册的路由或默认网络栈。每个 Route 只能被处理一次Route is already handled!即由_checkNotHandled()抛出多条规则按后注册先执行LIFO的顺序尝试匹配。进阶篇先发真实请求再修改响应有些场景下测试必须让 API 真实调用一次例如依赖服务端生成的数据、或需要验证请求副作用只要求对响应做局部打补丁以便输出确定可断言的结果。此时思路与上面相反仍然用route()拦截但在 handler 中先调用route.fetch()拿到原始响应解析并修改 body 后再用route.fulfill({ response, json })回填。下面的示例向 fruits 列表追加了一个名为 Loquat 的新水果test(gets the json from api and adds a new fruit, async ({ page }) { // Get the response and add to it await page.route(*/**/api/v1/fruits, async route { const response await route.fetch(); const json await response.json(); json.push({ name: Loquat, id: 100 }); // Fulfill using the original response, while patching the response body // with the given JSON object. await route.fulfill({ response, json }); }); // Go to the page await page.goto(https://demo.playwright.dev/api-mocking); // Assert that the new fruit is visible await expect(page.getByText(Loquat, { exact: true })).toBeVisible(); });async def test_gets_the_json_from_api_and_adds_a_new_fruit(page: Page): async def handle(route: Route): response await route.fetch() json await response.json() json.append({ name: Loquat, id: 100}) # Fulfill using the original response, while patching the response body # with the given JSON object. await route.fulfill(responseresponse, jsonjson) await page.route(https://demo.playwright.dev/api-mocking/api/v1/fruits, handle) # Go to the page await page.goto(https://demo.playwright.dev/api-mocking) # Assert that the new fruit is visible await expect(page.get_by_text(Loquat, exactTrue)).to_be_visible()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(responseresponse, jsonjson) 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, exactTrue)).to_be_visible()await page.RouteAsync(*/**/api/v1/fruits, async (route) { var response await route.FetchAsync(); var fruits await response.JsonAsyncFruit[](); fruits.Add(new Fruit() { Name Loquat, Id 100 }); // Fulfill using the original response, while patching the response body // with the given JSON object. await route.FulfillAsync(new () { Response response, Json fruits }); } ); // Go to the page await page.GotoAsync(https://demo.playwright.dev/api-mocking); // Assert that the Loquat fruit is visible await Expect(page.GetByText(Loquat, new () { Exact true })).ToBeVisibleAsync();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)); // Fulfill using the original response, while patching the response body // with the given JSON object. route.fulfill(new Route.FulfillOptions().setResponse(response).setBody(parsed.toString())); }); // Go to the page page.navigate(https://demo.playwright.dev/api-mocking); // Assert that the Loquat fruit is visible assertThat(page.getByText(Loquat, new Page.GetByTextOptions().setExact(true))).isVisible();在上述用例的 trace 中可以看到 API 确实被调用了但返回给页面的 body 已被改写Loquat 被追加进数组。源码层面的行为说明route.fetch()的本质是让Route内部的APIRequestContext重新执行一次请求packages/playwright-core/src/client/network.ts#L350-L354因此它会绕过已注册的 route 拦截拿到服务端最原始、未经 mock 的响应。而fulfill({ response, json })会继承该 response 的状态码与响应头仅替换 body——这正是真实调用 局部修改语义的实现基础。HAR 篇录制并回放完整的网络会话当页面依赖的接口数量庞大几十个 XHR/静态资源时逐个写route规则不现实。Playwright 提供routeFromHAR()让你用HARHTTP Archive文件一次搞定多条请求的仿真。所谓 HAR 文件是按 HTTP Archive 1.2 规范 记录页面加载过程中所有网络请求的档案包含请求/响应头、Cookie、响应内容、时间线等信息。使用 HAR 的完整流程分三步录制一个 HAR 文件将 HAR 文件与测试一起提交到版本库在测试中用routeFromHAR按保存的 HAR路由/回放请求。录制 HAR 文件录制通过page.routeFromHAR()或browserContext.routeFromHAR()完成。该方法接收 HAR 路径和可选 optionsurlglob 模式字符串或正则。指定后只有 URL 匹配的请求从 HAR 供给不指定则所有请求都从 HAR 供给update: true此时不播放 HAR而是把本次实际网络流量写入新建或更新HAR 文件。首次编写用例时用它把真实数据灌入 HAR。在 JS 中还有一种替代录制方式创建 BrowserContext 时传入recordHar选项捕获整个 context 生命周期内的全部网络流量直到 context 关闭。Python / Java / C# 中对应选项名为recordHarPath。test(records or updates the HAR file, async ({ page }) { // Get the response from the HAR file await page.routeFromHAR(./hars/fruit.har, { url: */**/api/v1/fruits, update: true, }); // Go to the page await page.goto(https://demo.playwright.dev/api-mocking); // Assert that the fruit is visible await expect(page.getByText(Strawberry)).toBeVisible(); });async def test_records_or_updates_the_har_file(page: Page): # Get the response from the HAR file await page.route_from_har(./hars/fruit.har, url*/**/api/v1/fruits, updateTrue) # Go to the page await page.goto(https://demo.playwright.dev/api-mocking) # Assert that the fruit is visible await expect(page.get_by_text(Strawberry)).to_be_visible()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, updateTrue) # 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()// Get the response from the HAR file await page.RouteFromHARAsync(./hars/fruit.har, new () { Url */**/api/v1/fruits, Update true, }); // Go to the page await page.GotoAsync(https://demo.playwright.dev/api-mocking); // Assert that the fruit is visible await Expect(page.GetByText(Strawberry)).ToBeVisibleAsync();// Get the response from the HAR file page.routeFromHAR(Path.of(./hars/fruit.har), new RouteFromHAROptions() .setUrl(*/**/api/v1/fruits) .setUpdate(true) ); // Go to the page page.navigate(https://demo.playwright.dev/api-mocking); // Assert that the fruit is visible assertThat(page.getByText(Strawberry)).isVisible();注意在update: true的录制模式下handler 会走tracing._recordIntoHAR流程packages/playwright-core/src/client/page.ts#L574-L585录制的是 context 中实际产生的网络流量此时 goto 的页面与数据应保证是值得固化的真实数据。用 CLI 录制 HAR官方推荐优先使用update: true在测试中录制因为它天然对应当前测试所访问的 URL不过也可以使用 Playwright CLI 手动录制# Save API requests from example.com as example.har archive. npx playwright open --save-harexample.har --save-har-glob**/api/** https://example.com# Save API requests from example.com as example.har archive. mvn exec:java -e -D exec.mainClasscom.microsoft.playwright.CLI -D exec.argsopen --save-harexample.har --save-har-glob**/api/** https://example.com# Save API requests from example.com as example.har archive. playwright open --save-harexample.har --save-har-glob**/api/** https://example.com# Save API requests from example.com as example.har archive. pwsh bin/Debug/netX/playwright.ps1 open --save-harexample.har --save-har-glob**/api/** https://example.com参数说明open以普通非自动化模式打开浏览器手动浏览从而产生真实流量--save-harpathHAR 输出路径。若以.zip结尾资源会被拆分为独立文件并压缩进单个 zip详见下文--save-har-globglob可选过滤只保存你关心的请求例如只留**/api/**。修改 HAR 文件里的 mock 数据录制完成后的 HAR 可以在hars目录中找到对应名称的hashed带哈希命名.txt文件直接编辑其中 JSON 即可改数据例如把一个水果改名为 Playwright[ { name: Playwright, id: 100 }, // ... other fruits ]这份修改后的文件应提交到版本库成为测试的固定档。之后只要再用update: true跑一次HAR 又会用真实的 API 响应覆盖更新。从 HAR 回放录制好并改完档后在测试中移除或置为 falseupdate选项即可从 HAR 中回放匹配的响应完全不再访问真实 API。匹配不到请求时默认行为是 abort可通过notFound: fallback改为放行到网络栈。test(gets the json from HAR and checks the new fruit has been added, async ({ page }) { // Replay API requests from HAR. // Either use a matching response from the HAR, // or abort the request if nothing matches. await page.routeFromHAR(./hars/fruit.har, { url: */**/api/v1/fruits, update: false, }); // Go to the page await page.goto(https://demo.playwright.dev/api-mocking); // Assert that the Playwright fruit is visible await expect(page.getByText(Playwright, { exact: true })).toBeVisible(); });async 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. await page.route_from_har(./hars/fruit.har, url*/**/api/v1/fruits, updateFalse) # Go to the page await page.goto(https://demo.playwright.dev/api-mocking) # Assert that the Playwright fruit is visible await expect(page.get_by_text(Playwright, exactTrue)).to_be_visible()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, updateFalse) # 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, exactTrue)).to_be_visible()// Replay API requests from HAR. // Either use a matching response from the HAR, // or abort the request if nothing matches. await page.RouteFromHARAsync(./hars/fruit.har, new () { Url */**/api/v1/fruits, Update false, } ); // Go to the page await page.GotoAsync(https://demo.playwright.dev/api-mocking); // Assert that the Playwright fruit is visible await page.ExpectByTextAsync(Playwright, new() { Exact true }).ToBeVisibleAsync();// Replay API requests from HAR. // Either use a matching response from the HAR, // or abort the request if nothing matches. page.routeFromHAR(Path.of(./hars/fruit.har), new RouteFromHAROptions() .setUrl(*/**/api/v1/fruits) .setUpdate(false) ); // Go to the page page.navigate(https://demo.playwright.dev/api-mocking); // Assert that the Playwright fruit is visible assertThat(page.getByText(Playwright, new Page.GetByTextOptions() .setExact(true))).isVisible();从回放测试的 trace 可以看到请求确实由 HAR 供给并 fulfilledAPI 服务器全程未被调用同时响应体中已包含我们手工写入的 Playwright 水果数据。HAR 回放的匹配规则文档 源码双重印证官方文档明确给出如下匹配语义HAR 回放严格匹配 URL 与 HTTP 方法对 POST 请求还严格匹配 POST payload请求体若多个录制条目都能匹配同一请求则取匹配请求头最多的一条命中 3xx 重定向的条目会被自动跟进到最终 URL。上述规则与 packages/playwright-core/src/server/harBackend.ts#L96-L150 的_harFindResponse实现逐条对应URL/方法不相等直接淘汰POST 会对 postData 做字节级比较并对 multipart boundary 做了容差处理随后用countMatchingHeaders选出 header 命中数最多的候选遇到 301/302/303/307/308 且有location头时按 fetch 规范改写 method 后跟进。客户端侧的查找与处理封装在 packages/playwright-core/src/client/harRouter.ts#L47-L110。关于.zip归档的补充与录制时相同若routeFromHAR传入的 HAR 文件名以.zip结尾Playwright 会将其视为HAR 文件 独立网络 payload 条目的归档容器。你也可以把 zip 解压出来直接编辑 payload 或 HAR 日志再指向解压后的 har 文件使用——此时所有 payload 都按该 har 文件在文件系统上的相对位置解析。对应的端到端验证可参考仓库测试 tests/library/browsercontext-har.spec.ts其中覆盖了context.routeFromHAR/page.routeFromHAR的 URL 过滤、notFound: abort | fallback、重定向跟随、update 录制等行为。进阶Mock WebSockets真实项目中 WebSocket 长连接同样会引入不确定性。Playwright 提供page.routeWebSocket()对 WS/WSS 连接做两层控制完全离线仿真拦截连接直接在测试内扮演服务器完成整个通信页面根本不连真实服务器消息代理页面真实连上服务器但中间消息可被截获、改写或阻断。模式一完整模拟 WebSocket 服务器下面的代码拦截wss://example.com/ws凡是收到文本request就回发responseawait page.routeWebSocket(wss://example.com/ws, ws { ws.onMessage(message { if (message request) ws.send(response); }); });page.routeWebSocket(wss://example.com/ws, ws - { ws.onMessage(frame - { if (request.equals(frame.text())) ws.send(response); }); });def message_handler(ws: WebSocketRoute, message: Union[str, bytes]): if message request: ws.send(response) await page.route_web_socket(wss://example.com/ws, lambda ws: ws.on_message( lambda message: message_handler(ws, message) ))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) ))await page.RouteWebSocketAsync(wss://example.com/ws, ws { ws.OnMessage(frame { if (frame.Text request) ws.Send(response); }); });消息既可以是字符串也可以是二进制Buffer/bytes。若 handler 未在事件回调里发送任何消息Playwright 会保证 WebSocket 仍被置为 open 状态、可正常发送——客户端侧由_afterHandle()调用ensureOpened保证packages/playwright-core/src/client/network.ts#L580-L586。模式二连接真实服务器并在中间改消息若希望建立真实连接、但篡改中途消息可先调用ws.connectToServer()获得代表服务器侧的代理对象server然后在ws.onMessage中决定把原消息透传还是替换后再发给服务器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); }); });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()); }); });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)) await page.route_web_socket(wss://example.com/ws, handler)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)await page.RouteWebSocketAsync(wss://example.com/ws, ws { var server ws.ConnectToServer(); ws.OnMessage(frame { if (frame.Text request) server.Send(request2); else server.Send(frame.Text); }); });从源码 packages/playwright-core/src/client/network.ts#L460-L587 可以看到WebSocketRoute的双向事件模型onMessage对应页面 → mock/服务器方向的回调来自messageFromPage事件server.onMessage对应服务器 → 页面方向messageFromServer事件connectToServer()每个 route 只能调用一次重复调用抛Already connected to the server返回的 server 代理对象负责把消息发回页面。另有url()、protocols()、close(code?, reason?)可用于断言与主动关闭。更多细节见 Playwright 的WebSocketRoute类型参考。更多阅读与仓库线索更复杂的网络处理header 改写、请求继续转发、取消与卸载路由等见文档 docs/src/network.md高级网络会话控制能力集中于BrowserContext/Page的 route 相关 API客户端实现可查看 packages/playwright-core/src/client/browserContext.ts 与 packages/playwright-core/src/client/page.ts仓库内置大量 HAR 场景资源可拿来实验例如 tests/assets/har-fulfill.har、tests/assets/har-redirect.har多语言端到端覆盖见 tests/library/browsercontext-har.spec.ts、tests/library/browsercontext-route.spec.ts 等测试文件。小结综合以上内容Playwright 的 Mock 能力构成了一个由浅入深的确定性测试工具箱page.routeroute.fulfill完全拦截请求并注入自定义响应适合快速造数、模拟异常route.fetchroute.fulfill({ response, json })真实请求 响应打补丁兼顾服务端真实性与断言确定性routeFromHAR录制 → 改档 → 回放整段网络会话严格匹配 URL / 方法 / POST body让包含大量接口的页面也能离线复现routeWebSocket仿真或代理 WebSocket把长连接通信也纳入可控范围。无论前端应用调用的是普通 XHR/fetch 接口还是 WebSocketPlaywright 都能让测试在完全不依赖外部服务的前提下稳定运行这正是其一个 API 测试 Chromium / Firefox / WebKit理念在网络层的有力体现。【免费下载链接】playwrightPlaywright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.项目地址: https://gitcode.com/GitHub_Trending/pl/playwright创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表