Playwright网络请求拦截与Mock实战:提升自动化测试效率与稳定性 1. 项目概述为什么我们需要拦截与Mock做自动化测试或者爬虫的朋友肯定都遇到过这样的场景你写了一个完美的脚本信心满满地跑起来结果页面加载到一半一个第三方广告接口超时了整个测试卡住或者你想测试一个“提交订单”的功能但总不能每次都真金白银地下单吧又或者后端接口还没开发好前端同学想先联调一下界面逻辑。这些问题本质上都指向同一个需求我们需要对网络请求进行精细化的控制。Playwright作为一个现代浏览器自动化工具其网络请求拦截Interception与模拟Mock能力正是为了解决这些痛点而生的利器。它不像Selenium那样对网络层几乎“放任自流”也不像单纯的单元测试Mock库那样脱离浏览器环境。Playwright允许你在真实的浏览器上下文中像交警一样指挥网络流量可以截停任意请求检查它的“证件”请求头和体然后决定是放行、修改、还是直接给它一个“伪造的通行证”Mock响应。简单来说这个功能让你从被动的“等待页面加载完成”变成了主动的“定义页面应该加载什么”。无论是屏蔽干扰请求以提升测试稳定性还是模拟各种边界条件如慢速、失败、异常数据来验证前端健壮性亦或是进行前后端并行开发时的接口Mock都离不开它。接下来我们就深入拆解Playwright如何实现这些能力并分享一些实战中总结出来的“骚操作”和避坑指南。2. 核心能力拆解路由、拦截与Mock的三位一体很多人会把拦截Intercept和Mock混为一谈其实在Playwright的体系里它们是紧密相关但层次分明的两个概念。理解这个层次是灵活运用的前提。2.1 路由Route网络流量的总调度台你可以把page.route()或context.route()看作是在浏览器和网络之间设立的一个检查站。所有匹配特定URL模式的请求在发往服务器之前都会先经过这个检查站。# 在页面上下文中设置一个路由拦截所有图片请求 await page.route(**/*.{png,jpg,jpeg}, lambda route: route.abort())这段代码的作用是为这个页面实例注册一个路由规则匹配所有以.png,.jpg,.jpeg结尾的请求。当这样的请求发生时Playwright不会让它真正发出去而是交给一个处理函数这里是lambda表达式来决定它的命运。route.abort()就是直接中止这个请求相当于告诉浏览器“此路不通”常用于屏蔽图片、字体、广告脚本等非必要资源极大加速测试执行。路由的核心价值在于“匹配”和“拦截”。它定义了“抓谁”和“在哪个阶段抓”。你可以基于URL、资源类型通过request.resource_type()判断等多种条件进行精准匹配。2.2 请求与响应对象Request Response流量详情单一旦请求被路由拦截你就可以通过route.request对象拿到这个请求的所有信息url: 请求的目标地址。method: GET、POST等。headers: 请求头包含Cookie、User-Agent等。post_data: 对于POST请求这里就是请求体body。resource_type: 判断是documentHTML、stylesheetCSS、scriptJS还是image等。同样如果你选择让请求继续并获取真实响应或者你自己构造了一个Mock响应你会用到Response对象或其模拟体关注status: 状态码200, 404, 500等。headers: 响应头。body: 响应体通常是JSON、HTML或二进制数据。理解这两个对象是进行任何高级操作的基础。比如你想Mock一个登录接口就必须知道它期望的请求方法POST、请求体格式通常是JSON然后才能伪造一个正确的响应。2.3 Mock响应伪造的通行证这是最激动人心的部分。拦截请求后你不一定要中止它还可以给它发一个“假的”响应这就是Mock。await page.route( **/api/login, lambda route: route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.dumps({success: True, token: fake-jwt-token-123}) ) )这里我们拦截了登录接口并直接使用route.fulfill()方法返回了一个成功的JSON响应。浏览器会认为它真的从服务器收到了这个响应从而触发前端相应的逻辑如跳转首页、存储token。Mock的精髓在于“以假乱真”。你需要根据前端代码的预期构造出格式、状态码、头部都完全匹配的响应数据。这让你可以在后端不可用、不稳定或需要特定数据场景时依然能顺畅地进行前端测试或开发。3. 实战进阶从基础拦截到复杂场景模拟掌握了基本概念我们来看几个实战中高频出现的场景和对应的代码实现。我会在代码中加入大量注释解释每一步的意图和注意事项。3.1 场景一性能优化与稳定性提升——屏蔽非必要资源这是最直接的应用。一个现代网页加载了太多第三方资源分析脚本、广告、字体、大图。在自动化测试中它们不仅拖慢速度还可能因为网络波动导致测试失败。import asyncio from playwright.async_api import async_playwright async def main(): async with async_playwright() as p: browser await p.chromium.launch(headlessFalse) context await browser.new_context() page await context.new_page() # 在上下文级别拦截对该上下文下所有页面生效 await context.route( # 匹配模式使用通配符**匹配任意路径屏蔽特定类型的资源 **/*.{css,woff,woff2,ttf,eot,svg,png,jpg,jpeg,gif,ico,mp4,webm}, lambda route: route.abort() # 直接中止请求 ) # 特别注意谨慎屏蔽.js除非你确认该脚本不影响核心功能测试。 # 许多页面的交互逻辑依赖JS盲目屏蔽会导致页面功能失效。 try: # 访问一个新闻网站观察加载速度 await page.goto(https://example-news-site.com) # 可以在这里截图对比屏蔽前后的加载完成时间 await page.screenshot(pathpage_without_resources.png) # 通过 page.evaluate 计算页面加载时间等性能指标 load_time await page.evaluate(() performance.timing.loadEventEnd - performance.timing.navigationStart) print(f页面加载时间屏蔽资源后: {load_time}ms) except Exception as e: print(f访问页面时出错: {e}) finally: await browser.close() asyncio.run(main())注意route.abort()有一个可选参数error_code可以模拟网络错误如abort(‘failed’)模拟失败abort(‘timedout’)模拟超时。这在测试前端错误处理逻辑时非常有用。3.2 场景二接口Mock——前后端分离开发的利器假设你在开发一个商品列表页后端分页接口还没好。你可以用Playwright Mock一个本地数据。import json from playwright.sync_api import sync_playwright # 使用同步API示例 def mock_product_list(): with sync_playwright() as p: browser p.chromium.launch(headlessFalse) context browser.new_context() page context.new_page() # 拦截获取商品列表的API def handle_route(route): # 1. 首先可以打印或检查真实的请求信息便于调试 request route.request print(f拦截到请求: {request.method} {request.url}) # 例如检查查询参数 if page in request.url: print(请求中包含分页参数) # 2. 构造Mock响应数据 mock_data { code: 0, msg: success, data: { list: [ {id: 1, name: Mock商品A, price: 99.9}, {id: 2, name: Mock商品B, price: 199.9}, # ... 可以构造更多数据测试分页 ], total: 25, page: 1, pageSize: 10 } } # 3. 履行请求返回Mock数据 route.fulfill( status200, headers{Content-Type: application/json; charsetutf-8}, # 注意charset bodyjson.dumps(mock_data, ensure_asciiFalse) # ensure_asciiFalse确保中文不乱码 ) # 使用更精确的URL匹配避免误拦截 page.route(**/api/products*, handle_route) # 匹配所有以/api/products开头的请求 page.goto(http://localhost:8080/product-list.html) # 你的前端本地地址 # 此时页面调用的 /api/products 接口将收到我们伪造的数据 page.wait_for_timeout(5000) # 等待页面渲染实际应用中应用更智能的等待 browser.close() # 执行 mock_product_list()实操心得Mock数据时响应头的Content-Type一定要和真实接口保持一致。很多前端框架如axios会根据这个头来解析数据。如果是JSON通常就是application/json。加上charsetutf-8能更好地处理中文。body需要是字符串所以要用json.dumps()转换。3.3 场景三修改请求与响应——更精细的控制有时你不想完全接管请求只是想“微调”一下。修改请求比如在所有请求头上加一个特定的认证Token。await page.route(**/*, lambda route: route.continue_(headers{ **route.request.headers, # 保留原有headers Authorization: Bearer my-fake-token # 添加新header }))这里用了route.continue_()意思是“修改后继续放行”请求会带着新的头部发往真实服务器。修改响应这需要先让请求继续然后捕获响应并进行修改。Playwright没有直接的route.continue_并修改响应的API但可以通过组合fetch请求实现。async def modify_response(route): # 1. 先继续请求获取原始响应 response await route.fetch() # 这里会真正发起网络请求 # 2. 获取原始响应体 original_body await response.text() # 3. 修改响应体例如给所有返回的标题加上[MODIFIED]前缀 # 注意这里假设响应是JSON。如果是其他格式需要相应处理。 try: body_json json.loads(original_body) if title in body_json: body_json[title] f[MODIFIED] {body_json[title]} modified_body json.dumps(body_json) except json.JSONDecodeError: # 如果不是JSON按文本处理谨慎操作 modified_body original_body \n!-- Modified by Playwright -- # 4. 用修改后的内容履行Mock这个请求 await route.fulfill( responseresponse, # 继承原始响应的状态码、大部分头部等 bodymodified_body # 覆盖响应体 ) await page.route(**/api/article/*, modify_response)这个技巧非常强大可以用于A/B测试、数据脱敏、或者动态注入调试信息。但要注意route.fetch()会发起真实网络请求只适用于你允许且能够访问的真实后端。4. 高阶技巧与避坑指南玩转拦截和Mock光会基础操作还不够。下面这些是我在项目中踩过坑后总结的经验。4.1 路由匹配模式精准打击的艺术Playwright的路由匹配支持多种模式用对了才能指哪打哪。page.route(**/api/**, handler): 双星号**匹配任意路径段包括零个这是最常用的通配符。page.route(**/*.{png,jpg}, handler): 匹配特定扩展名。page.route(*/api/user, handler): 单星号*匹配任意单个路径段如/v1/api/user或/v2/api/user。page.route(**/api/user?id123, handler):注意默认情况下URL模式不包含查询参数?之后的部分。要匹配带查询参数的需要检查route.request.url全路径。使用函数进行编程式匹配最灵活def complex_matcher(url, resource_type): return google-analytics in url and resource_type script await page.route(complex_matcher, handler)踩坑记录我曾想屏蔽所有google-analytics.com的请求用了模式**/*google-analytics*结果漏掉了一些。后来发现有些URL是https://www.google-analytics.com/ga.js有些是https://ssl.google-analytics.com/...。最稳妥的方式是使用函数匹配判断if ‘google-analytics’ in url。4.2 处理顺序与优先级谁先谁后你可以为同一个页面注册多个路由。Playwright会按照注册的先后顺序依次尝试匹配并使用第一个匹配成功的路由的处理程序。# 注册一个宽泛的规则屏蔽所有图片 await page.route(**/*.{png,jpg}, lambda route: route.abort()) # 注册一个更具体的规则对某个特定logo图片放行 await page.route(**/logo.png, lambda route: route.continue_()) # 访问一个包含 /logo.png 的页面 # 结果logo.png 也会被第一个规则拦截并中止因为第一个规则先注册且匹配成功。结论先注册的规则优先级高。因此应该先注册具体规则后注册通用规则。把上面的两行代码顺序调换就能实现“屏蔽除logo外的所有图片”。4.3 异步处理与竞态条件处理函数handler可以是异步的。这在需要从外部文件读取Mock数据或进行异步计算时非常有用。async def async_mock_handler(route): # 模拟一个耗时的操作比如读取文件 await asyncio.sleep(0.1) mock_data await read_mock_file(data.json) await route.fulfill(jsonmock_data) # route.fulfill 也支持直接传json对象 await page.route(**/api/data, async_mock_handler)但要小心竞态条件。如果你在page.goto()之后才设置路由那么页面初始加载时发出的请求可能已经错过拦截。最佳实践是在导航之前就设置好路由。# 正确做法 page await context.new_page() await page.route(**/api/config, handler) # 先设置路由 await page.goto(https://myapp.com) # 后导航 # 风险做法 await page.goto(https://myapp.com) # 导航时config接口请求可能已经发出 await page.route(**/api/config, handler) # 此时设置已晚4.4 启用与禁用路由动态控制你可以在测试的不同阶段动态启用或禁用Mock。# 设置路由但先不启用 route await page.route(**/api/test, handler, times1) # times1 表示只处理一次 # 执行某些操作... # 需要时再启用实际上设置即启用。更常见的需求是“取消”路由。 await route.abort() # 不对这是中止单个请求 # 正确的动态控制方式是使用条件判断或在处理函数中“放行” mock_enabled True async def conditional_handler(route): if mock_enabled: await route.fulfill(status404, bodyMocked Not Found) else: await route.continue_() route await page.route(**/api/test, conditional_handler) # 在测试中可以通过修改 mock_enabled 变量来控制行为更彻底的方法是使用page.unroute()来移除路由。# 移除所有路由 await page.unroute(**/api/test) # 或者移除指定处理程序的路由 await page.unroute(**/api/test, handlerconditional_handler)5. 集成测试实战构建一个健壮的Mock测试套件让我们把这些知识点串联起来看一个接近真实项目的例子测试一个电商网站的“加入购物车”功能并Mock掉所有依赖的后端接口。import pytest import json from playwright.sync_api import Page, expect # 假设的测试数据 MOCK_PRODUCT_DETAIL { id: 123, name: Playwright实战指南, price: 66.6, stock: 100, description: 一本好书 } MOCK_ADD_TO_CART_RESPONSE { code: 0, msg: 添加成功, data: {cartItemId: cart_001} } MOCK_CART_COUNT_RESPONSE { code: 0, data: {count: 5} } pytest.fixture(scopefunction) def set_up_mocks(page: Page): 为每个测试用例设置通用的Mock路由 # Mock 1: 商品详情接口 def mock_product_detail(route): route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.dumps(MOCK_PRODUCT_DETAIL, ensure_asciiFalse) ) page.route(**/api/product/123, mock_product_detail) # Mock 2: 加入购物车接口 def mock_add_to_cart(route): # 这里可以验证请求体是否正确 request route.request if request.method POST: try: post_data json.loads(request.post_data or {}) # 断言前端发送的数据符合预期 assert post_data.get(productId) 123 assert post_data.get(quantity) 1 except json.JSONDecodeError: pass # 或者处理错误情况 route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.dumps(MOCK_ADD_TO_CART_RESPONSE) ) page.route(**/api/cart/add, mock_add_to_cart) # Mock 3: 购物车数量接口 def mock_cart_count(route): route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.dumps(MOCK_CART_COUNT_RESPONSE) ) page.route(**/api/cart/count, mock_cart_count) # 屏蔽所有分析脚本和图片提升测试速度 page.route(**/*.{png,jpg,gif,woff,woff2}, lambda route: route.abort()) page.route(**/analytics.js, lambda route: route.abort()) yield # 执行测试用例 # 测试结束后可以清理路由非必须因为page会关闭 # page.unroute(**/api/product/123) def test_add_to_cart_happy_path(page: Page, set_up_mocks): 测试正常加入购物车流程 # 1. 导航到商品详情页 (依赖 Mock 1) page.goto(http://localhost:8080/product/123) # 验证页面正确显示了Mock的商品信息 product_name page.locator(.product-name) expect(product_name).to_have_text(MOCK_PRODUCT_DETAIL[name]) expect(page.locator(.product-price)).to_contain_text(str(MOCK_PRODUCT_DETAIL[price])) # 2. 点击加入购物车按钮 (会触发 Mock 2) # 首先监听一下网络请求用于断言 with page.expect_request(**/api/cart/add) as request_info: page.click(button:has-text(加入购物车)) request request_info.value # 可选断言请求方法 assert request.method POST # 3. 验证前端交互按钮状态变化、成功提示等 success_toast page.locator(.toast-success) expect(success_toast).to_be_visible() expect(success_toast).to_contain_text(添加成功) # 4. 验证购物车角标数量更新 (依赖 Mock 3) # 注意前端可能在添加成功后自动调用购物车数量接口 cart_badge page.locator(.cart-badge) expect(cart_badge).to_have_text(str(MOCK_CART_COUNT_RESPONSE[data][count])) def test_add_to_cart_out_of_stock(page: Page, set_up_mocks): 测试库存不足的情况 # 动态修改Mock模拟库存为0 def mock_product_no_stock(route): out_of_stock_data MOCK_PRODUCT_DETAIL.copy() out_of_stock_data[stock] 0 route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.dumps(out_of_stock_data) ) # 覆盖之前设置的商品详情Mock page.route(**/api/product/123, mock_product_no_stock) page.goto(http://localhost:8080/product/123) # 验证“加入购物车”按钮是禁用状态或显示“缺货” add_button page.locator(button:has-text(加入购物车)) expect(add_button).to_be_disabled() # 或者 # expect(page.locator(.out-of-stock-tag)).to_be_visible()这个例子展示了如何将Mock集成到Pytest测试框架中通过fixture统一管理Mock并在不同测试用例中灵活覆盖Mock行为从而测试各种业务场景。6. 常见问题排查与调试技巧即使掌握了所有API在实际操作中还是会遇到各种奇怪的问题。这里记录几个我常遇到的坑和解决方法。问题1Mock没有生效请求还是走到了真实服务器。检查匹配模式最可能的原因是URL模式没匹配上。使用console.log(route.request.url)在处理函数里打印一下实际拦截到的URL看看和你预期的模式是否一致。特别注意查询参数和哈希#是不包含在路由匹配中的。检查注册时机确保在请求发出前通常是page.goto()或触发请求的操作之前就注册了路由。在page.on(‘request’)事件监听器里打印所有请求URL可以帮助你理清请求顺序。检查是否被其他路由先处理如前所述路由有顺序。可能有一个更早注册的通用路由比如**/*已经处理如abort或continue_了这个请求。问题2Mock了响应但页面显示异常或JS报错。检查响应头尤其是Content-Type。如果应该是application/json但你返回了text/plain前端可能无法解析。用浏览器开发者工具的Network面板对比Mock响应和真实响应的Headers差异。检查响应体格式确保JSON是有效的、格式正确的。特别是中文字符使用json.dumps(..., ensure_asciiFalse)。对于非JSON响应如HTML片段确保字符串格式正确。检查状态码有些前端代码会检查HTTP状态码比如只处理200而你Mock了一个404。查看浏览器控制台错误打开headlessFalse模式运行测试直接看控制台有没有JS报错这是最直接的线索。问题3异步操作导致Mock响应顺序错乱。如果你的Mock处理函数里有await比如读文件要确保整个处理是同步完成的或者前端能处理稍晚一点的响应。对于关键的首屏接口Mock逻辑应尽量简单快速。考虑使用route.fulfill()的json参数直接传递Python字典避免手动json.dumps。问题4如何调试复杂的请求/响应使用route.continue_()和开发者工具暂时不Mock只是让请求继续但在处理函数里打印详细的请求信息方法、头、体。然后到浏览器开发者工具的Network面板里查看真实的请求和响应这是你构造Mock数据的最佳参考。利用page.on(‘request’)和page.on(‘response’)事件它们可以监听所有请求和响应即使没有被路由拦截非常适合用来了解页面完整的网络活动图谱。page.on(request, lambda request: print(f {request.method} {request.url})) page.on(response, lambda response: print(f {response.status} {response.url}))问题5处理二进制响应如图片、PDFroute.fulfill()的body参数可以接受bytes类型。# Mock一个1x1像素的透明GIF图片 transparent_gif base64.b64decode(R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7) await page.route(**/fake-image.gif, lambda route: route.fulfill( status200, content_typeimage/gif, bodytransparent_gif ))掌握Playwright的网络请求拦截与Mock相当于给你的自动化脚本装上了“上帝之手”。你可以任意塑造网络环境从而专注于测试前端逻辑本身让测试更快、更稳定、更全面。从简单的资源屏蔽到复杂的接口模拟这套工具链能覆盖绝大多数测试场景。关键在于理解其工作原理并勤加练习在实战中积累匹配模式、数据构造和问题排查的经验。