Playwright网络请求拦截与模拟实战:从原理到六大应用场景详解
2026/7/6 9:04:44 网站建设 项目流程

1. 项目概述与核心价值

如果你正在用 Playwright 做自动化测试或者数据抓取,有没有遇到过这样的场景:测试一个下单流程,但支付接口总是不稳定,导致你的测试脚本动不动就失败;或者你想测试前端页面在网络超时或服务器返回错误时的表现,却苦于无法“制造”这种异常;又或者,你只是想屏蔽页面上那些加载缓慢的图片和广告,让脚本跑得更快一些。这些问题,本质上都是对网络请求流的控制需求。而 Playwright 提供的网络请求拦截与模拟(Interception & Mocking)功能,正是解决这些痛点的“瑞士军刀”。它允许你在浏览器发起请求和接收响应的关键节点进行干预,修改请求、伪造响应,甚至直接阻断请求,从而构建一个完全可控、可预测的测试或执行环境。这不仅仅是测试领域的利器,对于需要精确控制网络行为的爬虫、监控脚本乃至前端调试,都极具价值。

简单来说,这个功能让你从被动的“请求-等待-响应”模式,转变为主动的“定义-控制-验证”模式。你不用再祈祷后端服务永远稳定,也不用忍受缓慢的第三方资源。通过拦截与模拟,你可以确保每一次自动化执行都在你设定的剧本中进行。接下来,我将结合多年实战经验,从原理到细节,带你彻底掌握这项核心技能。

2. 网络请求拦截与模拟的核心原理

要玩转拦截与模拟,首先得理解 Playwright 是如何运作的。它不像一些工具只是在浏览器外层套个代理,而是通过 Chrome DevTools Protocol 或类似的底层协议,与浏览器内核建立了深度通信通道。当你在代码中调用page.route(url, handler)时,你实际上是在告诉 Playwright:“嘿,帮我监听所有发往这个 URL 模式的请求,在它真正发出去之前,先交给我这个处理函数(handler)看看”。

2.1 路由(Route)与请求生命周期

每个被拦截的请求都会封装成一个Route对象。这个对象是你的操作入口,它包含了请求的所有信息(URL、方法、头信息、POST数据等),并提供了三个关键方法来决定这个请求的最终命运:

  1. route.continue_([options]):放行这个请求。你可以选择性地修改一些参数(如头信息、POST数据)后再放行。这适用于你想给所有请求添加一个认证 Token,或者修改某个查询参数的场景。
  2. route.fulfill([options]):立即终止这个请求的发送,并直接返回一个你自定义的响应(Mock 响应)。这是模拟(Mock)的核心。你可以指定状态码、响应头、响应体,完全伪造一个服务器的回复。
  3. route.abort([errorCode]):直接中止这个请求,模拟请求失败。比如,你可以用它来屏蔽图片、字体或广告脚本的加载,以提升性能或净化页面。

注意:对于每一个被拦截的请求,你必须handler函数中调用以上三个方法之一,否则请求会一直挂起,导致脚本超时。这是新手最容易踩的坑。

2.2 拦截的作用域:Page vs Context

理解作用域对构建清晰的测试架构至关重要。

  • page.route():拦截作用域仅限于该页面(Page)。在这个页面内发起的所有符合规则的请求都会被拦截。页面关闭后,拦截自动失效。适用于针对特定页面流程的 Mock。
  • context.route():拦截作用域是整个浏览器上下文(Context)。该上下文下所有页面(包括新打开的标签页)的请求都会被拦截。适用于需要全局生效的规则,比如全局的认证头注入、统一的资源屏蔽策略。

通常,我建议在conftest.py或全局 setup 中使用context.route()设置一些基础规则,在具体的测试用例中使用page.route()进行更精细的场景模拟。

2.3 与事件监听(page.on(‘request’))的区别

很多人会混淆route和事件监听。page.on(‘request’, handler)是在请求已经发出后触发的事件,你只能“看”,不能“改”。它常用于监控和记录网络活动。而page.route()是在请求即将发出但尚未发出时进行拦截,你拥有修改或响应的权力。一个是观察者,一个是决策者。

3. 实战技巧:六大典型应用场景拆解

理论说再多不如实际操练。下面我通过六个最常见的场景,展示具体的代码和背后的思考。

3.1 场景一:动态修改请求参数(如注入认证Token)

这是最基础也最常用的场景。假设你的应用需要在每个 API 请求的 Header 里带上一个 Bearer Token,而这个 Token 可能在测试环境是动态生成的。

import re from playwright.sync_api import sync_playwright def test_with_auth_token(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() # 定义一个路由处理器 def inject_auth_header(route): # 获取原始请求头 headers = route.request.headers.copy() # 重要:先复制一份 # 注入或覆盖认证头 headers['Authorization'] = f'Bearer your_dynamic_token_here' # 也可以添加其他通用头,如追踪ID headers['X-Request-ID'] = 'test_12345' # 修改后继续请求 route.continue_(headers=headers) # 注册路由,拦截所有API请求 # 使用 **/api/** 这样的模式匹配所有层级的API路径 page.route('**/api/**', inject_auth_header) page.goto('https://your-app.com') # 此时,页面内所有向 /api/ 发起的请求都会自动带上你注入的Token page.click('#load-data-btn') # 触发一个API调用 # ... 后续断言 browser.close()

实操心得

  • route.request.headers是一个字典,但直接修改它有时会遇到不可变对象的错误。安全起见,总是先.copy()一份再修改。
  • 使用通配符**可以灵活匹配路径。**/api/**会匹配https://domain.com/api/usershttps://domain.com/v1/api/orders
  • 对于POST请求,你还可以通过route.continue_(post_data=modified_data)来修改请求体。

3.2 场景二:拦截并返回静态Mock数据

当你想测试前端界面在不同数据下的表现,或者后端接口尚未开发完成时,这个功能就是救星。

import json from playwright.sync_api import sync_playwright def test_mock_user_api(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() def mock_user_response(route): # 精确匹配用户详情接口 if route.request.url.endswith('/api/user/123'): mock_data = { "id": 123, "name": "Mocked User", "email": "mock@example.com", "status": "active" } # 使用fulfill返回模拟响应 route.fulfill( status=200, headers={"Content-Type": "application/json"}, body=json.dumps(mock_data) ) else: # 对于不匹配的请求,正常放行 route.continue_() page.route('**/api/user/**', mock_user_response) page.goto('https://your-app.com/user/123') # 页面将显示你模拟的用户数据,而非真实后端数据 # 可以断言页面元素是否根据mock数据正确渲染 assert page.text_content('.user-name') == 'Mocked User' browser.close()

注意事项

  • route.fulfillbody参数需要是字符串。如果是 JSON,记得用json.dumps()转换。
  • 明确设置Content-Type响应头非常重要,尤其是application/json,这样浏览器才会正确解析。
  • handler函数里一定要有else: route.continue_()的逻辑,除非你确定要拦截该模式下的所有请求。否则,不匹配的请求会被挂起。

3.3 场景三:模拟异常场景(网络错误、服务端错误)

健壮性测试离不开异常模拟。Playwright 可以轻松模拟4xx、5xx状态码或网络断开。

def test_server_error_handling(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() def simulate_server_error(route): if '/api/payment' in route.request.url: # 模拟服务器内部错误 route.fulfill( status=500, body=json.dumps({"error": "Internal Server Error", "code": "ERR-500"}) ) elif '/api/checkout' in route.request.url: # 模拟网络超时(通过abort模拟失败) # 这里选择abort,并指定错误码为'timedout' route.abort('timedout') else: route.continue_() page.route('**/api/**', simulate_server_error) page.goto('https://your-app.com/checkout') page.click('#submit-payment') # 此时,前端应该触发你的错误处理逻辑,比如显示错误提示框 # 断言错误信息是否如期显示 assert page.is_visible('.error-toast') assert "支付服务暂时不可用" in page.text_content('.error-toast') browser.close()

排查技巧

  • route.abort()可以接受错误码,如'failed','aborted','timedout','accessdenied'等,模拟不同类型的网络失败。
  • 结合page.on(‘requestfailed’)事件监听器,可以捕获并记录这些被中止的请求,便于调试。

3.4 场景四:替换或屏蔽页面资源(加速测试)

测试页面性能或只想关注核心功能时,屏蔽图片、样式、字体等非关键资源可以极大提升执行速度。

def test_blocking_resources(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() # 方法1:直接中止特定类型的请求 def block_media(route): # 使用正则表达式匹配资源后缀 if route.request.resource_type in ['image', 'stylesheet', 'font', 'media']: route.abort() else: route.continue_() page.route('**/*', block_media) # 方法2:更精确的URL模式匹配(推荐) # 使用正则表达式匹配所有.jpg, .png, .css, .woff2文件 import re page.route(re.compile(r".*\.(jpg|png|css|woff2)$"), lambda route: route.abort()) page.goto('https://news-site.com') # 页面会快速加载,因为大部分静态资源被阻止了 print(f"页面加载完成,DOMContentLoaded时间会显著缩短") browser.close()

性能优化建议

  • 在 CI/CD 流水线中运行大量测试时,全局启用资源屏蔽(通过context.route)可以节省可观的时间。
  • 小心不要屏蔽掉你的测试所依赖的 JS 文件或 API 接口。可以通过更精细的 URL 匹配来排除。

3.5 场景五:修改响应内容(动态篡改)

有时你不想完全接管响应,只是想对真实的响应内容做点“小手术”,比如修改某个字段的值用于测试。

def test_modify_response(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() async def modify_api_response(route): # 首先,继续请求并获取真实响应 response = await route.fetch() # 注意:这里用了异步fetch # 获取原始响应体 original_body = await response.text() original_json = json.loads(original_body) # 篡改数据:例如,把所有价格打五折用于测试 if 'price' in original_json: original_json['price'] = original_json['price'] * 0.5 if 'items' in original_json and isinstance(original_json['items'], list): for item in original_json['items']: if 'price' in item: item['price'] = item['price'] * 0.5 # 用修改后的数据履行响应 await route.fulfill( response=response, # 继承原始响应的状态和头信息 body=json.dumps(original_json) # 提供新的响应体 ) # 注意:由于handler是async的,需要用异步API await page.route('**/api/products/**', modify_api_response) await page.goto('https://your-app.com/products') # 页面上显示的商品价格都将是半价 browser.close()

重要提示route.fetch()是一个异步操作,它会让请求真正发生一次,拿到响应后再交给你处理。这在你需要基于真实响应进行修改时非常有用,但会带来网络开销。如果你的目的只是纯 Mock,直接用route.fulfill构造数据性能更好。

3.6 场景六:基于请求参数的动态Mock

高级Mock场景需要根据请求的不同参数返回不同的响应。这需要从请求对象中解析信息。

def test_dynamic_mock_based_on_query(): with sync_playwright() as p: browser = p.chromium.launch(headless=False) page = browser.new_page() def dynamic_mock_handler(route): request = route.request url = request.url # 解析查询参数 from urllib.parse import urlparse, parse_qs parsed_url = urlparse(url) query_params = parse_qs(parsed_url.query) user_id = query_params.get('userId', [None])[0] if '/api/user' in parsed_url.path and user_id: # 根据不同的userId返回不同的Mock数据 if user_id == '1001': mock_data = {"id": 1001, "role": "admin"} elif user_id == '1002': mock_data = {"id": 1002, "role": "user", "blocked": True} else: mock_data = {"id": int(user_id), "role": "guest", "error": "not found"} route.fulfill( status=200, content_type="application/json", body=json.dumps(mock_data) ) else: route.continue_() page.route('**/api/user*', dynamic_mock_handler) # 匹配 /api/user 及其所有查询参数 page.goto('https://your-app.com/profile?userId=1002') # 页面将接收到id为1002的“被封锁”用户数据 browser.close()

4. 工程化实践:如何组织你的Mock代码

当Mock逻辑变得复杂时,散落在各个测试用例里会是一场灾难。下面分享我常用的几种组织模式。

4.1 模式一:Pytest Fixture 封装

这是最推荐的方式,利用 Pytest 的 fixture 机制,实现 Mock 逻辑的复用和生命周期管理。

# conftest.py import pytest import json from playwright.sync_api import Page @pytest.fixture def mock_user_api(page: Page): """Fixture:模拟用户相关API""" def handler(route): if route.request.url.endswith('/api/current-user'): route.fulfill( status=200, body=json.dumps({"name": "Fixture User", "id": 999}) ) else: route.continue_() # 在测试开始前注册路由 page.route('**/api/current-user', handler) yield # 测试在此处执行 # 测试结束后,可以清理(通常Playwright会自动清理,但显式清理是好习惯) page.unroute('**/api/current-user', handler) # test_dashboard.py def test_dashboard_with_mock(mock_user_api, page: Page): """测试用例会自动应用mock_user_api这个fixture""" page.goto('https://app.com/dashboard') # 页面使用的 /api/current-user 接口已被Mock assert page.text_content('.welcome-msg') == 'Welcome, Fixture User!'

4.2 模式二:Mock服务类

对于大型项目,可以将不同领域的 Mock 逻辑封装成类,便于管理和扩展。

# mocks/user_mock.py class UserAPIMock: def __init__(self, page): self.page = page self._active_handlers = [] def enable_basic_profile(self): """启用基础用户信息Mock""" def handler(route): route.fulfill(status=200, body=json.dumps(self._basic_profile)) self.page.route('**/api/user/profile', handler) self._active_handlers.append(('**/api/user/profile', handler)) def enable_permissions(self, role='user'): """根据角色Mock权限接口""" permissions_map = { 'user': ['read'], 'admin': ['read', 'write', 'delete'] } def handler(route): route.fulfill( status=200, body=json.dumps({"permissions": permissions_map.get(role, [])}) ) self.page.route('**/api/user/permissions', handler) self._active_handlers.append(('**/api/user/permissions', handler)) def disable_all(self): """禁用所有由这个类注册的Mock""" for url_pattern, handler in self._active_handlers: self.page.unroute(url_pattern, handler) self._active_handlers.clear() @property def _basic_profile(self): return {"name": "Mocked User", "email": "mock@test.com"} # 在测试中使用 def test_with_mock_service(page: Page): user_mock = UserAPIMock(page) user_mock.enable_basic_profile() user_mock.enable_permissions(role='admin') page.goto('https://app.com') # ... 执行测试 user_mock.disable_all() # 测试后清理

4.3 模式三:基于环境变量的开关控制

在真实测试中,你可能希望只在特定环境(如本地开发、CI)启用Mock,而在预发布环境使用真实服务。

# conftest.py import os import pytest def pytest_configure(config): # 读取环境变量,决定是否启用全局Mock config.MOCK_ENABLED = os.getenv('ENABLE_PLAYWRIGHT_MOCK', 'false').lower() == 'true' @pytest.fixture(scope='session') def mock_config(request): return {'enabled': request.config.MOCK_ENABLED} @pytest.fixture def auto_mock(page: Page, mock_config): if mock_config['enabled']: # 应用一系列基础Mock def mock_common_apis(route): if '/api/config' in route.request.url: route.fulfill(body=json.dumps({"env": "test", "features": ["mock"]})) else: route.continue_() page.route('**/api/config', mock_common_apis) print("全局Mock已启用") yield

然后在运行测试时通过环境变量控制:ENABLE_PLAYWRIGHT_MOCK=true pytest

5. 高级技巧与调试指南

掌握了基础用法和工程化实践后,一些高级技巧和调试方法能让你如虎添翼。

5.1 处理异步请求与等待

有时,你的操作会触发一个异步请求(如点击按钮后前端发起AJAX),你需要等待这个特定的被Mock的请求完成。

def test_wait_for_mocked_request(page: Page): # 先设置Mock page.route('**/api/submit', lambda route: route.fulfill(status=200, body='OK')) page.goto('https://app.com/form') # 方法1:使用 page.wait_for_request # 在触发动作前,开始等待一个匹配的请求 with page.expect_request('**/api/submit') as req_info: page.click('#submit-button') request = req_info.value print(f"捕获到请求: {request.method} {request.url}") # 可以在这里对请求对象进行断言 assert request.post_data is not None # 方法2:使用 page.wait_for_response (等待Mock响应) with page.expect_response('**/api/submit') as resp_info: page.click('#submit-button') response = resp_info.value print(f"收到响应: {response.status}") # 注意:因为被Mock了,这里的response是你fulfill返回的响应

5.2 利用正则表达式进行复杂匹配

通配符***有时不够精确,正则表达式能提供强大的匹配能力。

import re # 匹配所有包含特定域名且以 /v2/ 开头的API pattern1 = re.compile(r'https:\/\/api\.example\.com\/v2\/.*') page.route(pattern1, handler1) # 匹配所有用户ID为数字的详情页请求 pattern2 = re.compile(r'.*\/user\/\d+\/profile$') page.route(pattern2, handler2) # 匹配所有查询参数中包含 `debug=true` 的请求 pattern3 = re.compile(r'.*\?.*debug=true.*') page.route(pattern3, handler3)

5.3 网络监控与调试

在开发Mock逻辑时,实时看到所有网络请求非常有帮助。

# 在页面初始化后立即添加监听器 page.on('request', lambda request: print(f'→ {request.method} {request.url}')) page.on('response', lambda response: print(f'← {response.status} {response.url}')) page.on('requestfailed', lambda request: print(f'✗ {request.url} {request.failure}')) # 更结构化的日志记录 def log_network(enable=True): if enable: def on_request(request): if '/api/' in request.url: # 只记录API请求 print(f'[REQ] {request.method} {request.url}') def on_response(response): if '/api/' in response.url: print(f'[RES] {response.status} {response.url}') page.on('request', on_request) page.on('response', on_response)

5.4 常见问题排查技巧实录

在实际操作中,我踩过不少坑,这里总结几个高频问题:

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

  • 检查点1:URL匹配是否正确。这是最常见的原因。使用page.on(‘request’, …)打印出实际发出的请求URL,与你route()中写的模式仔细对比。注意协议(http/https)、域名、路径末尾的斜杠。
  • 检查点2:拦截注册时机。必须在页面发起导航(page.goto)或触发请求的操作之前调用page.route()。通常建议在page.goto之前就设置好。
  • 检查点3:Handler函数是否正确调用 continue_ 或 fulfill。确保你的 handler 函数对所有分支都有处理,没有漏掉的请求。

问题2:测试卡住,一直不结束。

  • 原因:几乎可以肯定是有请求被拦截了,但你的 handler 函数没有调用route.continue_(),route.fulfill()route.abort()中的任何一个。请求被挂起了。
  • 解决:检查你的 handler 函数逻辑,确保所有可能的代码路径最终都调用了上述三个方法之一。使用try...except包裹 handler 主体,并在异常时调用route.continue_()是个保险的做法。
def safe_handler(route): try: # 你的Mock逻辑 if should_mock(route.request): route.fulfill(...) else: route.continue_() except Exception as e: print(f"处理请求 {route.request.url} 时出错: {e}") route.continue_() # 出错时放行,避免阻塞测试

问题3:修改了请求头,但服务端没收到。

  • 原因:浏览器对某些请求头(如Host,Content-Length等)有保护,不允许通过脚本修改。此外,如果请求是跨域的且未通过 CORS 预检,修改某些头也会被浏览器静默忽略。
  • 解决:避免修改敏感头。对于跨域问题,Mock 本身通常在同域下进行,如果涉及跨域 Mock,可能需要更复杂的设置,或者考虑直接使用route.fulfill完全模拟响应。

问题4:异步API(playwright.async_api)下的使用差异。

  • 关键点:Handler 函数必须是异步的(async def),并且在调用route.continue_(),route.fulfill()时需要await
  • 示例:
    async def async_handler(route): if await should_mock_async(route.request): await route.fulfill(status=200, body='Mocked') else: await route.continue_() await page.route('**/api/test', async_handler)

6. 性能考量与最佳实践

滥用请求拦截也会带来性能开销。遵循以下实践,可以让你的脚本既强大又高效。

  1. 精确匹配,避免全局拦截:尽量不要使用page.route(‘**/*’, handler)。这会强制 Playwright 检查每一个请求(包括 favicon.ico、各种像素跟踪等),造成不必要的开销。尽量使用具体的 URL 模式或正则表达式。
  2. 在Context级别设置通用规则:如果多个页面都需要相同的 Mock(如认证头、资源屏蔽),使用context.route()而不是在每个page上重复设置。
  3. 及时清理:如果一个 Mock 只针对单个测试用例,在用例结束后使用page.unroute(pattern, handler)将其移除,防止污染后续测试。
  4. 优先使用route.fulfill而非route.fetch() + modify如果目的是纯模拟,直接构造响应数据比先请求再修改要快得多,且不依赖网络。
  5. Mock数据的轻量化:返回的 Mock 响应体不要过大,尤其是模拟列表数据时,够用即可。巨大的 JSON 响应会消耗内存和解析时间。
  6. 结合录制功能生成Mock:Playwright Codegen 或 Trace Viewer 可以记录网络请求。你可以先运行一次真实流程,从记录中复制真实的请求和响应数据,稍作修改作为你的 Mock 数据,这样既真实又快捷。

网络请求拦截与模拟是 Playwright 从中级迈向高级使用的标志性功能。它赋予了你对测试环境的终极控制权,将脆弱的、依赖外部服务的测试,转变为稳定、快速、可重复的可靠资产。从简单的请求修改到复杂的动态 Mock 服务,这项技术的应用边界只取决于你的想象力。花时间掌握它,你构建的自动化解决方案的稳健性和专业性将提升一个数量级。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询