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

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: 判断是document(HTML)、stylesheet(CSS)、script(JS)还是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( status=200, headers={"Content-Type": "application/json"}, body=json.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(headless=False) 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(path="page_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(headless=False) 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( status=200, headers={"Content-Type": "application/json; charset=utf-8"}, # 注意charset body=json.dumps(mock_data, ensure_ascii=False) # ensure_ascii=False确保中文不乱码 ) # 使用更精确的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。加上charset=utf-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( response=response, # 继承原始响应的状态码、大部分头部等 body=modified_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?id=123", 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(json=mock_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, times=1) # times=1 表示只处理一次 # 执行某些操作... # 需要时再启用(实际上,设置即启用)。更常见的需求是“取消”路由。 await route.abort() # 不对,这是中止单个请求 # 正确的动态控制方式是:使用条件判断或在处理函数中“放行” mock_enabled = True async def conditional_handler(route): if mock_enabled: await route.fulfill(status=404, body="Mocked 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", handler=conditional_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(scope="function") def set_up_mocks(page: Page): """为每个测试用例设置通用的Mock路由""" # Mock 1: 商品详情接口 def mock_product_detail(route): route.fulfill( status=200, headers={"Content-Type": "application/json"}, body=json.dumps(MOCK_PRODUCT_DETAIL, ensure_ascii=False) ) 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( status=200, headers={"Content-Type": "application/json"}, body=json.dumps(MOCK_ADD_TO_CART_RESPONSE) ) page.route("**/api/cart/add", mock_add_to_cart) # Mock 3: 购物车数量接口 def mock_cart_count(route): route.fulfill( status=200, headers={"Content-Type": "application/json"}, body=json.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( status=200, headers={"Content-Type": "application/json"}, body=json.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,在实际操作中还是会遇到各种奇怪的问题。这里记录几个我常遇到的坑和解决方法。

问题1:Mock没有生效,请求还是走到了真实服务器。

  • 检查匹配模式:最可能的原因是URL模式没匹配上。使用console.log(route.request.url)在处理函数里打印一下实际拦截到的URL,看看和你预期的模式是否一致。特别注意查询参数和哈希(#)是不包含在路由匹配中的。
  • 检查注册时机:确保在请求发出前(通常是page.goto()或触发请求的操作之前)就注册了路由。在page.on(‘request’)事件监听器里打印所有请求URL,可以帮助你理清请求顺序。
  • 检查是否被其他路由先处理:如前所述,路由有顺序。可能有一个更早注册的通用路由(比如**/*)已经处理(如abortcontinue_)了这个请求。

问题2:Mock了响应,但页面显示异常或JS报错。

  • 检查响应头:尤其是Content-Type。如果应该是application/json但你返回了text/plain,前端可能无法解析。用浏览器开发者工具的Network面板,对比Mock响应和真实响应的Headers差异。
  • 检查响应体格式:确保JSON是有效的、格式正确的。特别是中文字符,使用json.dumps(..., ensure_ascii=False)。对于非JSON响应(如HTML片段),确保字符串格式正确。
  • 检查状态码:有些前端代码会检查HTTP状态码,比如只处理200,而你Mock了一个404。
  • 查看浏览器控制台错误:打开headless=False模式,运行测试,直接看控制台有没有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:处理二进制响应(如图片、PDF)

  • route.fulfill()body参数可以接受bytes类型。
# Mock一个1x1像素的透明GIF图片 transparent_gif = base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7") await page.route("**/fake-image.gif", lambda route: route.fulfill( status=200, content_type="image/gif", body=transparent_gif ))

掌握Playwright的网络请求拦截与Mock,相当于给你的自动化脚本装上了“上帝之手”。你可以任意塑造网络环境,从而专注于测试前端逻辑本身,让测试更快、更稳定、更全面。从简单的资源屏蔽到复杂的接口模拟,这套工具链能覆盖绝大多数测试场景。关键在于理解其工作原理,并勤加练习,在实战中积累匹配模式、数据构造和问题排查的经验。

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

相关文章:

  • 3分钟学会PS修图:模糊的照片变清晰零基础通用教程
  • 从合同系统到财务入账:业财一体化的 4 个关键节点
  • 计算机毕业设计之 基于微信小程序的生鲜系统的设计与实现
  • [特殊字符]《淘宝开放平台个人开发者 vs 企业开发者权限与接口差异对比》(附Python源码)
  • configure 完整使用手册(零基础·全覆盖·可直接查阅)
  • 【IDEA极速部署手册】:从下载到运行Hello World仅需137秒——含自动环境检测脚本(GitHub Star 2.4k)
  • 非技术创业者如何用AI快速验证App创业项目
  • 南安普顿大学补考想转国内?这份申请攻略收好
  • Switch device-crx:高效模拟多设备,解决Web跨平台兼容性测试痛点
  • 谷歌收录及流量恢复帮助:尚未建索引?干预7天就出结果
  • GLM-4.7-Flash 量化版本地部署,1 张 4090 开跑
  • 5分钟快速上手:Balena Etcher - 最安全的跨平台镜像烧录工具终极指南
  • 从深思洛克到Virbox的软件安全演进
  • 3步轻松搞定Windows 11系统优化:告别臃肿,重获流畅体验
  • IntelliJ IDEA安装后中文乱码、Maven不识别、Git路径失效?——全栈工程师的12项初始化校准清单(含registry配置密钥)
  • 空间站构型升级背后的技术刚需:硬实时操作系统筑牢航天测控根基
  • Okbiye 数据分析功能:零基础搞定实证研究,自动生成可直接复用的论文数据报告
  • 全球覆盖广的海关数据哪个好用
  • 程序员面试“外挂“哪家强?2026年度10款AI面试工具全维度实测
  • 【Mac开发者必备指南】:2024最新IntelliJ IDEA安装全流程(含M1/M2芯片适配避坑清单)
  • 一键清掉C盘30G!这款C盘垃圾专清工具,让你彻底告别C盘不够用!
  • Javascript闭包的理解
  • 三分钟掌握Umi-CUT:批量图片去黑边的自动化解决方案
  • IntelliJ IDEA旗舰版安装常见陷阱全曝光:许可证绑定失效、Proxy劫持、Java 21兼容性断点(附JetBrains Support团队内部调试日志截图)
  • 每日热门skill:别手动做PPT了!这个OpenClaw Skill让我每天省出3小时,数据分析+PPT一键搞定
  • 如何彻底告别网盘限速:9大平台直链下载加速终极指南
  • Gamdl:用命令行下载 Apple Music 的全部内容
  • Blender 3MF插件终极指南:如何在Blender中实现3D打印文件无缝导入导出
  • Windows 11终极优化指南:用Win11Debloat免费清理系统臃肿
  • 3步永久解锁IDM:免费激活Internet Download Manager完整教程