当前位置: 首页 > news >正文

Playwright Route API 实战:网络请求拦截与 Mock 接口测试

1. 项目概述:为什么我们需要拦截和修改网络请求?

在自动化测试和爬虫开发的日常工作中,我们经常会遇到一个令人头疼的问题:测试环境依赖的第三方接口不稳定。比如,你正在为一个电商应用编写下单流程的自动化测试脚本,脚本需要调用支付网关的接口来验证支付成功后的页面跳转。然而,这个支付网关的测试环境可能时不时返回500错误,或者响应速度极慢,甚至直接不可用。这会导致你的测试脚本频繁失败,而失败的原因并非你的应用代码有问题,而是外部依赖的“猪队友”拖了后腿。

另一种常见场景是,你需要测试应用在接收到特定异常数据时的表现。例如,你想知道当用户账户余额接口返回一个负数时,前端页面是会显示“余额异常”还是直接崩溃。但在正常情况下,你几乎不可能让真实的第三方服务给你返回一个负数的余额。这时候,如果能“欺骗”一下浏览器,让它以为接收到的响应数据就是我们预设的,问题就迎刃而解了。

Playwright 的 Route API 就是为了解决这类问题而生的利器。它允许我们在浏览器页面发起网络请求时,进行拦截,并可以做出三种选择:继续请求、中止请求,或者提供一个自定义的响应。这个功能的核心价值在于,它将测试脚本的稳定性和可控性从外部环境的束缚中解放了出来。我们不再需要跪求第三方服务提供稳定的测试环境,也不用在测试数据准备上大费周章。通过 Mock(模拟)接口响应,我们可以精准地构造出各种正常、边界乃至异常的场景,从而对我们的应用进行更全面、更可靠的验证。

简单来说,掌握了 Route API,你就相当于在浏览器和网络之间安装了一个“过滤器”和“应答机”。你可以查看所有进出的“包裹”(请求和响应),可以决定是否放行,甚至可以自己伪造一个“包裹”塞回去。这对于前端开发、测试工程师以及需要处理复杂网络交互的爬虫开发者来说,是一项不可或缺的核心技能。

2. Route API 核心原理与能力拆解

要玩转 Route API,首先得理解它在 Playwright 架构中的位置以及其核心的工作机制。这并非一个黑盒魔法,理解了原理,才能用得得心应手。

2.1 请求生命周期与路由拦截点

Playwright 对网络请求的管理非常细致。一个典型的 HTTP/HTTPS 请求在浏览器中会经历多个阶段。Route API 主要作用于“请求已发出,但尚未到达网络”的这个关键时刻。具体来说,当使用page.route(url, handler)browserContext.route(url, handler)方法注册一个路由时,Playwright 就开始监听匹配该 URL 模式的所有请求。

一旦请求被拦截,控制权就交给了我们提供的handler回调函数。这个函数会接收到一个Route对象。此时,请求就像被按下了暂停键,等待我们的指令。我们必须在handler中调用Route对象的以下三个方法之一来决定请求的最终命运:

  1. route.continue([overrides]): 放行请求,允许它继续发往目标服务器。我们可以选择性地修改请求头、请求方法(POST/GET等)甚至请求体(postData)。
  2. route.fulfill([options]): 立即终止请求,并直接返回一个我们自定义的响应。这是实现 Mock 接口最常用的方法,我们可以指定状态码、响应头和响应体。
  3. route.abort([errorCode]): 中止请求,模拟请求失败。可以指定失败类型,如‘aborted’(中止)、‘accessdenied’(拒绝访问)、‘timedout’(超时)等,用于测试网络错误场景。

这里有一个关键细节:handler函数必须是异步的(async),并且必须对每个被拦截的请求调用上述三个方法之一。如果忘记调用,或者因为异常导致没有调用,这个请求就会被永远挂起,导致页面加载卡死或超时。这是新手最容易踩的坑。

2.2 匹配模式:精准捕获目标请求

URL 匹配模式是路由的“触发器”。Playwright 支持多种匹配方式:

  • 字符串匹配page.route(‘https://api.example.com/user’, handler)只会拦截这个精确的 URL。
  • 通配符匹配:使用*匹配任意字符序列。例如,page.route(‘**/api/*/user*’, handler)可以匹配https://a.com/api/v1/user/profilehttps://b.com/api/v2/users
  • 正则表达式匹配:提供最灵活的匹配能力。例如,page.route(/\/api\/v\d+\/order\/\d+/, handler)可以匹配所有类似/api/v1/order/123/api/v2/order/456的路径。

注意:匹配模式过于宽泛(如**)可能会拦截到页面必需的资源(如 CSS、JS、图片),导致页面渲染异常。务必精确匹配,通常建议从浏览器的开发者工具“网络”标签中复制确切的请求 URL 或路径模式。

2.3 与类似工具的对比

你可能听说过其他工具也能修改网络请求,比如 Charles/Fiddler 等抓包工具,或者浏览器原生提供的window.fetchXMLHttpRequest重写。Route API 与它们有何不同?

  • vs. Charles/Fiddler:这些是系统级的代理工具,独立于浏览器和测试脚本。它们功能强大,但配置相对复杂,且难以与自动化测试流程集成。Route API 是代码级的、程序化的控制,可以直接写在测试脚本里,与测试用例的生命周期(如beforeEachafterEach)完美结合,实现动态、条件化的 Mock。
  • vs. 浏览器原生重写:通过page.evaluate注入代码来覆盖window.fetchXMLHttpRequest虽然可行,但这是一项“黑客”行为,可能破坏页面原有逻辑,且无法拦截通过<img><script>标签发起的请求。Route API 由 Playwright 在浏览器引擎底层实现,拦截更彻底、更稳定,不影响页面逻辑。

因此,Route API 的核心优势在于“可编程性”“集成性”,它让网络请求的 Mock 和修改成为了自动化测试脚本的一个有机组成部分。

3. 实战场景一:Mock 第三方接口响应

这是 Route API 最经典的应用场景。我们的目标是:当页面尝试调用一个不稳定的或我们无法控制的第三方接口时,直接返回我们预设好的“假”数据,让测试流程顺利进行下去。

3.1 基础 Mock:返回静态 JSON 数据

假设我们有一个用户中心页面,它会调用https://api.demo.com/user/info来获取当前用户的昵称和头像。我们想 Mock 这个接口,让它返回固定的用户信息。

const { chromium } = require(‘playwright’); (async () => { const browser = await chromium.launch({ headless: false }); const context = await browser.newContext(); const page = await context.newPage(); // 关键步骤:注册路由拦截器 await page.route(‘https://api.demo.com/user/info’, async route => { // 构造一个模拟的响应 const mockResponse = { status: 200, contentType: ‘application/json’, body: JSON.stringify({ userId: 10001, nickname: ‘测试用户’, avatar: ‘https://demo.com/avatar/default.jpg’ }) }; // 使用自定义响应完成此次请求 await route.fulfill(mockResponse); }); // 导航到页面,页面发起的 /user/info 请求将被拦截并返回上述 mock 数据 await page.goto(‘https://your-app.com/user-center’); // 此时,页面应该显示昵称为“测试用户” // ... 后续的断言和操作 await browser.close(); })();

实操心得

  • route.fulfillbody参数必须是字符串。如果是 JSON 对象,记得用JSON.stringify()转换。
  • contentType头非常重要。设置为‘application/json’能确保浏览器正确解析响应体。如果 Mock 的是 HTML 或文本,则需设置为‘text/html’‘text/plain’
  • 这个 Mock 是“一次性”的,只针对这个page对象生效。如果新开一个标签页(newPage),需要重新注册路由,或者使用browserContext.route()在上下文级别进行拦截,这样该上下文下的所有页面都会生效。

3.2 高级 Mock:动态响应与条件判断

真实的测试场景往往更复杂。我们可能需要根据请求的不同参数,返回不同的响应数据。

例如,一个搜索接口https://api.demo.com/search?keyword=xxx,当关键词为“playwright”时返回相关结果,为空时返回错误,为“error”时模拟服务器500错误。

await page.route(/https:\/\/api\.demo\.com\/search\?.*/, async route => { // 获取被拦截的请求对象 const request = route.request(); // 获取URL对象,方便解析查询参数 const url = new URL(request.url()); const keyword = url.searchParams.get(‘keyword’); let mockResponse; if (!keyword) { // 关键词为空,返回业务错误 mockResponse = { status: 400, body: JSON.stringify({ code: 40001, message: ‘关键词不能为空’ }) }; } else if (keyword === ‘error’) { // 模拟服务器内部错误 mockResponse = { status: 500, body: ‘Internal Server Error’ }; } else if (keyword === ‘playwright’) { // 返回特定关键词的模拟数据 mockResponse = { status: 200, contentType: ‘application/json’, body: JSON.stringify({ list: [ { id: 1, title: ‘Playwright 入门指南’ }, { id: 2, title: ‘Playwright Route API 详解’ } ], total: 2 }) }; } else { // 其他关键词,返回空结果 mockResponse = { status: 200, body: JSON.stringify({ list: [], total: 0 }) }; } await route.fulfill(mockResponse); });

这个例子展示了 Route API 的灵活性。我们可以在handler函数里编写任意逻辑,基于请求的 URL、方法、头信息甚至请求体来动态决定返回什么。这为测试诸如“分页加载”、“搜索过滤”、“表单提交验证”等交互逻辑提供了极大的便利。

3.3 集成到测试框架(Pytest/Jest)

在实际项目中,我们通常不会把 Mock 代码写在单个脚本里,而是集成到测试框架中,使其更模块化、可复用。

以 Pytest 为例,我们可以使用fixture来管理路由的注册和卸载:

import pytest from playwright.sync_api import Page, Route import json @pytest.fixture(scope=“function”) # 每个测试函数运行一次 def mock_user_api(page: Page): “””Mock 用户信息接口的 fixture””” def handle_route(route: Route): mock_data = {“userId”: 1001, “name”: “Fixture User”} route.fulfill(status=200, body=json.dumps(mock_data)) # 注册路由 page.route(“**/api/user”, handle_route) yield page # 将配置好 Mock 的 page 对象提供给测试用例 # 测试结束后,可以在这里清理路由(Playwright 通常会自动清理) def test_user_profile(mock_user_api): page = mock_user_api page.goto(“/user-profile”) # 断言页面显示了 Mock 的用户名 assert page.locator(“.user-name”).inner_text() == “Fixture User”

这样,每个需要 Mock 用户接口的测试用例,只需要引用这个fixture即可,代码非常清晰。对于 Jest 也是类似思路,可以使用beforeEach钩子来设置路由。

4. 实战场景二:修改请求与响应数据

除了完全 Mock,Route API 另一个强大功能是“修改”。即让请求正常发生,但在其发出前或返回后,对数据进行篡改。这常用于测试数据验证、敏感信息脱敏或模拟特定数据格式。

4.1 修改请求参数(Request)

有时我们需要测试后端对异常请求参数的处理,但前端页面无法产生这样的参数。这时可以在请求发出前修改它。

例如,测试一个登录接口,我们想验证当密码字段为空时,后端是否返回正确的错误提示。

await page.route(‘https://api.demo.com/login’, async route => { const request = route.request(); // 获取原始的请求数据(对于 POST 请求,通常是 JSON 或 FormData 字符串) const postData = request.postData(); let requestBody; try { requestBody = JSON.parse(postData); } catch { // 如果不是 JSON,可能是其他格式,这里简单处理 await route.continue(); return; } // 修改请求体,将密码置空 requestBody.password = ‘’; // 携带修改后的请求体继续请求 await route.continue({ postData: JSON.stringify(requestBody) }); });

注意事项

  • 修改postData时,必须确保其字符串格式与请求的Content-Type头匹配(如application/json)。
  • route.continue({overrides})还可以修改headersmethod。例如,你可以给所有请求添加一个特定的认证头,或者将某个 GET 请求改为 POST 请求进行测试。

4.2 修改响应数据(Response)

修改响应比完全 Mock 更“轻量”,它允许真实的请求到达服务器并返回,我们只是在响应数据返回给页面之前,对其进行加工。这需要用到route.continue()并配合page.on(‘response’)事件,但更优雅的方式是使用route.fulfill来“劫持并替换”响应。

更常见的模式是:先发起一个真实请求获取数据,然后修改它,最后将修改后的数据返回。这可以通过fetch或直接使用route.request().response()来实现,但后者在 Playwright 中获取原始响应稍显复杂。一个更直接的方法是结合route.continue()和响应事件监听,但逻辑会绕。

更实用的“修改响应”场景,其实可以看作是一种“先请求,后 Mock”的模式。例如,我们想让一个商品列表接口的每个商品价格都打五折,用于测试前端的价格展示逻辑。

await page.route(‘https://api.demo.com/products’, async route => { // 1. 先放行请求,获取原始响应 const response = await route.fetch(); // 这是 Playwright 提供的一个便捷方法 // 2. 获取原始响应体 const originalBody = await response.json(); // 3. 修改数据:所有价格打五折 const modifiedBody = originalBody.map(product => ({ ...product, price: product.price * 0.5 })); // 4. 用修改后的数据履行响应 await route.fulfill({ response, // 继承原始响应的状态码和大部分头信息 body: JSON.stringify(modifiedBody), headers: { ...response.headers(), // 继承原始头 ‘content-type’: ‘application/json’ // 确保 content-type 正确 } }); });

这里使用了route.fetch()方法,它会在后台发起一个请求(不经过页面),并返回响应对象,非常方便。这种方式既保证了请求的真实性(比如接口鉴权是有效的),又实现了对响应数据的定制化修改。

5. 常见问题排查与性能优化

在实际使用 Route API 的过程中,你可能会遇到一些意想不到的问题。下面是我总结的一些典型“坑”及其解决方案。

5.1 请求被挂起,页面卡住

现象:页面加载到一半不动了,开发者工具里看到某个请求一直处于Pending状态。原因:这是最常见的问题。你的路由handler函数没有调用route.continue()route.fulfill()route.abort()中的任何一个。可能是函数里有条件分支没有覆盖到,或者函数执行过程中抛出了异常。排查

  1. 确保handlerasync函数。
  2. handler函数内部用try...catch包裹核心逻辑,并在catch块中调用route.continue()作为保底。
  3. 检查所有逻辑分支,确保每个分支都结束了路由。
await page.route(‘**/api/**’, async route => { try { // 你的业务逻辑... if (someCondition) { await route.fulfill(/* ... */); } else { // 确保 else 分支也处理了路由 await route.continue(); } } catch (error) { console.error(‘路由处理出错:’, error); // 出错时也继续请求,避免卡死 await route.continue(); } });

5.2 Mock 未生效,请求依然走到了真实接口

现象:代码写了路由,但页面仍然发起了真实网络请求,并可能返回错误。原因

  1. URL 匹配错误:最常见。请求的 URL 与路由注册的模式不匹配。可能是协议(http/https)、域名、端口或路径的细微差别。
  2. 路由注册时机过晚:在page.goto()之后才注册路由。页面导航时发起的初始请求(如 HTML 文档内联的 API 调用)可能已经发出。
  3. 页面内发生了导航或跳转,跳转到了新的域名或路径,原有的路由匹配失效。排查
  4. 在路由handler里第一行打印route.request().url(),确认拦截到的 URL 是否符合预期。
  5. 将路由注册代码放在page.goto()之前。
  6. 对于单页应用(SPA)或可能跳转的页面,考虑使用browserContext.route()在更广的范围内拦截。

5.3 性能影响与优化建议

滥用路由拦截会对测试执行速度产生负面影响。每个匹配的请求都会增加 JavaScript 的执行开销。

优化建议

  1. 精确匹配:尽量使用最精确的 URL 字符串或正则表达式,避免使用**这样的宽泛模式拦截所有请求。
  2. 按需启用:不要在测试开始时全局注册所有 Mock 路由。使用 Pytest 的fixture或 Jest 的beforeEach,只为当前测试套件需要的接口注册路由。并在测试结束后及时清理(虽然 Playwright 的 Context 关闭时会自动清理)。
  3. 避免在路由 handler 中执行重型操作:比如进行复杂的计算或发起额外的网络请求。如果必须修改响应,且逻辑复杂,考虑将修改逻辑抽离成纯函数。
  4. 使用route.fetch()谨慎route.fetch()会额外发起一次网络请求,虽然方便,但增加了延迟。如果只是为了获取一个固定的 Mock 数据,不如直接构造。

5.4 处理 CORS 预检请求(OPTIONS)

当你 Mock 的接口域名与页面域名不同(跨域)时,浏览器可能会先发起一个OPTIONS方法的预检请求(Preflight Request)。如果你的路由只拦截了GETPOST,这个OPTIONS请求会被放行到真实服务器。如果真实服务器没有正确配置 CORS,可能会导致后续请求失败。

解决方案:在路由中同时处理OPTIONS请求,直接返回一个包含正确 CORS 头的响应。

await page.route(‘https://api.other-domain.com/**’, async route => { const request = route.request(); if (request.method() === ‘OPTIONS’) { // 处理预检请求 await route.fulfill({ status: 200, headers: { ‘Access-Control-Allow-Origin’: ‘*’, // 或具体的页面域名 ‘Access-Control-Allow-Methods’: ‘GET, POST, PUT, DELETE, OPTIONS’, ‘Access-Control-Allow-Headers’: ‘Content-Type, Authorization’, }, body: ‘’ // 预检响应体为空 }); } else { // 处理实际的 GET/POST 请求 // ... 你的 Mock 逻辑 await route.fulfill(/* ... */); } });

6. 复杂场景综合应用:一个完整的测试用例

让我们综合运用以上知识,设计一个相对完整的测试场景:测试一个“发表评论”的功能。

需求

  1. 页面加载时,从https://api.demo.com/article/123获取文章内容。
  2. 用户填写评论表单,点击提交,请求发送到https://api.demo.com/comment
  3. 提交成功后,页面会重新调用文章接口获取包含新评论的文章数据。

测试目标

  1. Mock 文章接口,返回一篇预设的文章。
  2. Mock 评论提交接口,模拟成功提交。
  3. 在评论提交成功后,修改下一次文章接口的响应,使其包含我们刚刚“提交”的评论,以验证页面刷新逻辑。
const { test, expect } = require(‘@playwright/test’); test(‘发表评论并刷新列表’, async ({ page }) => { let commentSubmitted = false; const mockArticle = { id: 123, title: ‘测试文章’, content: ‘…’, comments: [] }; // 拦截文章接口 await page.route(‘**/api/article/123’, async route => { if (commentSubmitted) { // 评论提交后,返回包含新评论的文章数据 const responseWithComment = { …mockArticle, comments: [{ id: 999, text: ‘这是一条测试评论’, user: ‘Tester’ }] }; await route.fulfill({ status: 200, contentType: ‘application/json’, body: JSON.stringify(responseWithComment) }); } else { // 首次加载,返回原始文章数据 await route.fulfill({ status: 200, contentType: ‘application/json’, body: JSON.stringify(mockArticle) }); } }); // 拦截评论提交接口 await page.route(‘**/api/comment’, async route => { const request = route.request(); const postData = JSON.parse(request.postData()); // 这里可以验证提交的数据,例如 expect(postData.text).toBe(‘这是一条测试评论’); // 模拟提交成功 commentSubmitted = true; // 设置标志位 await route.fulfill({ status: 200, body: JSON.stringify({ success: true, commentId: 999 }) }); }); // 导航到文章页 await page.goto(‘https://your-app.com/article/123’); // 验证文章标题 await expect(page.locator(‘.article-title’)).toHaveText(‘测试文章’); // 验证初始评论列表为空 await expect(page.locator(‘.comment-list li’)).toHaveCount(0); // 填写并提交评论 await page.locator(‘.comment-input’).fill(‘这是一条测试评论’); await page.locator(‘button[type=“submit”]’).click(); // 等待页面刷新评论列表(例如,等待新评论出现) await expect(page.locator(‘.comment-list li’)).toHaveCount(1); await expect(page.locator(‘.comment-list li:first-child’)).toContainText(‘这是一条测试评论’); });

这个例子展示了如何利用一个外部变量 (commentSubmitted) 在不同路由处理函数之间共享状态,从而模拟出“先提交,后刷新”的完整用户交互流程。这种模式在测试有状态变化的交互时非常有用。

Route API 的深度和灵活性远不止于此,结合 Playwright 的其他 API(如网络事件监听、请求/响应对象操作),你可以构建出极其复杂和强大的网络行为模拟,彻底掌控测试环境。关键在于理解其工作原理,并大胆地在实际项目中实践和组合这些模式。

http://www.jsqmd.com/news/1278912/

相关文章:

  • 暗黑破坏神2存档编辑器d2s-editor:零基础修改游戏存档的完整指南
  • Dify本地部署与工作流实战:构建企业级AI智能副驾
  • React Fragment核心原理与应用实践指南
  • 如何在AWS上快速部署自管理Kubernetes集群?Cluster API Provider AWS实战教程
  • 临沧市镇康县2026黄金回收门店避坑指南 白银回收铂金回收全城严选五家店铺上门服务商闭眼入 联系方式+地址 - 盛世金银回收
  • ed25519-dalek项目迁移公告:重要变更与新仓库地址全解析
  • 揭秘xmr-btc-swap核心技术:跨链原子交换协议的工作原理详解
  • Razor开发常见问题解决:Visual Studio与VS Code环境下的调试与排错技巧
  • 北京网站建设行业现状与优质服务商评估指南
  • AI模型发展趋势:从规模扩张到效率优化的关键转折
  • AI驱动测试优先级排序:基于代码变更风险的智能测试选择实践
  • VJ-FILTER-BOT终极指南:如何在5分钟内搭建你的专属自动过滤机器人
  • go-cqhttp完整指南:5分钟快速构建跨平台QQ机器人解决方案
  • 柳州市柳城县2026黄金回收门店避坑指南 白银回收铂金回收全城严选五家店铺上门服务商闭眼入 联系方式+地址 - 盛世金银回收
  • 英语前缀e-/ex-解析:核心含义与词汇拆解技巧
  • IIC协议深度解析:从时序原理到软件模拟与硬件调试实战
  • 从ERB到Slim:Slim-Rails让Rails视图开发效率提升300%的秘密
  • 深蓝词库转换:5分钟解决输入法词库格式不兼容难题
  • 如何快速集成android-EmojiCompat?3分钟实现表情符号显示
  • wxWidgets MDI应用开发:从环境搭建到文档视图框架实战
  • Docker 部署 go-nebulas 节点全攻略:简单高效的区块链环境搭建
  • 利用旧手机搭建IPv6博客服务器:Termux与Halo 2实践
  • iOS-Tagent性能优化指南:提升UI自动化测试效率的5个关键策略
  • TI电源路径管理芯片选型与设计实战:bq2403x/70/72系列对比解析
  • 深度学习文字生成技术:原理、应用与实战指南
  • 西门子S7-1200 PLC在停车场智能化改造中的应用
  • Snowflake Connector for Python性能优化指南:提升查询速度的7个终极方法
  • TI BQ20Z655电池管理芯片实战指南:从术语解析到工程调试
  • 九江市彭泽县2026黄金回收门店避坑指南 白银回收铂金回收全城严选五家店铺上门服务商闭眼入 联系方式+地址 - 大熊猫898989
  • 轨迹感知PRM技术提升大语言模型长链推理能力