尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

Playwright网络请求拦截与Mock实战:提升自动化测试效率与稳定性

Playwright网络请求拦截与Mock实战:提升自动化测试效率与稳定性
📅 发布时间:2026/6/26 8:07:42

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,可以帮助你理清请求顺序。
  • 检查是否被其他路由先处理:如前所述,路由有顺序。可能有一个更早注册的通用路由(比如**/*)已经处理(如abort或continue_)了这个请求。

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

相关新闻

  • 计算机毕业设计之 基于微信小程序的生鲜系统的设计与实现
  • [特殊字符]《淘宝开放平台个人开发者 vs 企业开发者权限与接口差异对比》(附Python源码)
  • 谷歌收录及流量恢复帮助:尚未建索引?干预7天就出结果

最新新闻

  • 嵌入式系统开发实践
  • 多模型动态路由(Fusion):从“算力霸权”到“架构分权”的工程范式转型
  • 从零到生产就绪,VMware+Ubuntu开发环境搭建全流程,含SSH、Docker、IDE远程调试配置
  • 2026年,专业永康别墅门供应商将带来怎样的品质与惊喜?
  • OFDM项目开发(08):OFDM系统中的循环前缀(CP)插入模块设计——基于Xilinx BRAM的Verilog实现
  • 【小白也能轻松用】轻量化纯净安装包,一键部署 OpenClaw v2.7.9 无多余繁琐配置步骤(最新安装包)

日新闻

  • Qwen2.5-Turbo百万上下文实战指南:百炼平台长文本处理全解析
  • 怎么监控对标账号更新,2026年作者监控工作流,5款深度对比
  • EdgeRemover:专业级Windows Edge浏览器管理工具,彻底解决顽固软件卸载难题

周新闻

  • Visual C++运行库修复终极指南:5分钟快速解决Windows软件启动错误
  • 手把手教你构建统计局地区经济数据爬虫:从环境搭建到数据持久化全指南
  • 2026多Agent深度解析:用AI团队替代单一模型,四种架构实战落地

月新闻

  • 【总结】入门篇:50句话让你记住架构核心概念
  • WeChatMsg技术方案解析:实现Mac微信数据自主管理的完整解决方案
  • WeChatMsg:革新性微信数据备份方案,打造你的专属数字记忆库

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号