1. 项目概述与核心价值如果你正在用 Playwright 做自动化测试或者数据抓取有没有遇到过这样的场景测试一个下单流程但支付接口总是不稳定导致你的测试脚本动不动就失败或者你想测试前端页面在网络超时或服务器返回错误时的表现却苦于无法“制造”这种异常又或者你只是想屏蔽页面上那些加载缓慢的图片和广告让脚本跑得更快一些。这些问题本质上都是对网络请求流的控制需求。而 Playwright 提供的网络请求拦截与模拟Interception Mocking功能正是解决这些痛点的“瑞士军刀”。它允许你在浏览器发起请求和接收响应的关键节点进行干预修改请求、伪造响应甚至直接阻断请求从而构建一个完全可控、可预测的测试或执行环境。这不仅仅是测试领域的利器对于需要精确控制网络行为的爬虫、监控脚本乃至前端调试都极具价值。简单来说这个功能让你从被动的“请求-等待-响应”模式转变为主动的“定义-控制-验证”模式。你不用再祈祷后端服务永远稳定也不用忍受缓慢的第三方资源。通过拦截与模拟你可以确保每一次自动化执行都在你设定的剧本中进行。接下来我将结合多年实战经验从原理到细节带你彻底掌握这项核心技能。2. 网络请求拦截与模拟的核心原理要玩转拦截与模拟首先得理解 Playwright 是如何运作的。它不像一些工具只是在浏览器外层套个代理而是通过 Chrome DevTools Protocol 或类似的底层协议与浏览器内核建立了深度通信通道。当你在代码中调用page.route(url, handler)时你实际上是在告诉 Playwright“嘿帮我监听所有发往这个 URL 模式的请求在它真正发出去之前先交给我这个处理函数handler看看”。2.1 路由Route与请求生命周期每个被拦截的请求都会封装成一个Route对象。这个对象是你的操作入口它包含了请求的所有信息URL、方法、头信息、POST数据等并提供了三个关键方法来决定这个请求的最终命运route.continue_([options])放行这个请求。你可以选择性地修改一些参数如头信息、POST数据后再放行。这适用于你想给所有请求添加一个认证 Token或者修改某个查询参数的场景。route.fulfill([options])立即终止这个请求的发送并直接返回一个你自定义的响应Mock 响应。这是模拟Mock的核心。你可以指定状态码、响应头、响应体完全伪造一个服务器的回复。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(headlessFalse) context browser.new_context() page context.new_page() # 定义一个路由处理器 def inject_auth_header(route): # 获取原始请求头 headers route.request.headers.copy() # 重要先复制一份 # 注入或覆盖认证头 headers[Authorization] fBearer your_dynamic_token_here # 也可以添加其他通用头如追踪ID headers[X-Request-ID] test_12345 # 修改后继续请求 route.continue_(headersheaders) # 注册路由拦截所有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/users和https://domain.com/v1/api/orders。对于POST请求你还可以通过route.continue_(post_datamodified_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(headlessFalse) 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: mockexample.com, status: active } # 使用fulfill返回模拟响应 route.fulfill( status200, headers{Content-Type: application/json}, bodyjson.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.fulfill的body参数需要是字符串。如果是 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(headlessFalse) page browser.new_page() def simulate_server_error(route): if /api/payment in route.request.url: # 模拟服务器内部错误 route.fulfill( status500, bodyjson.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(headlessFalse) 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(headlessFalse) 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( responseresponse, # 继承原始响应的状态和头信息 bodyjson.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(headlessFalse) 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( status200, content_typeapplication/json, bodyjson.dumps(mock_data) ) else: route.continue_() page.route(**/api/user*, dynamic_mock_handler) # 匹配 /api/user 及其所有查询参数 page.goto(https://your-app.com/profile?userId1002) # 页面将接收到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( status200, bodyjson.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(status200, bodyjson.dumps(self._basic_profile)) self.page.route(**/api/user/profile, handler) self._active_handlers.append((**/api/user/profile, handler)) def enable_permissions(self, roleuser): 根据角色Mock权限接口 permissions_map { user: [read], admin: [read, write, delete] } def handler(route): route.fulfill( status200, bodyjson.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: mocktest.com} # 在测试中使用 def test_with_mock_service(page: Page): user_mock UserAPIMock(page) user_mock.enable_basic_profile() user_mock.enable_permissions(roleadmin) 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(scopesession) 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(bodyjson.dumps({env: test, features: [mock]})) else: route.continue_() page.route(**/api/config, mock_common_apis) print(全局Mock已启用) yield然后在运行测试时通过环境变量控制ENABLE_PLAYWRIGHT_MOCKtrue pytest。5. 高级技巧与调试指南掌握了基础用法和工程化实践后一些高级技巧和调试方法能让你如虎添翼。5.1 处理异步请求与等待有时你的操作会触发一个异步请求如点击按钮后前端发起AJAX你需要等待这个特定的被Mock的请求完成。def test_wait_for_mocked_request(page: Page): # 先设置Mock page.route(**/api/submit, lambda route: route.fulfill(status200, bodyOK)) 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(rhttps:\/\/api\.example\.com\/v2\/.*) page.route(pattern1, handler1) # 匹配所有用户ID为数字的详情页请求 pattern2 re.compile(r.*\/user\/\d\/profile$) page.route(pattern2, handler2) # 匹配所有查询参数中包含 debugtrue 的请求 pattern3 re.compile(r.*\?.*debugtrue.*) 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(enableTrue): 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 常见问题排查技巧实录在实际操作中我踩过不少坑这里总结几个高频问题问题1Mock 没有生效请求还是走到了真实服务。检查点1URL匹配是否正确。这是最常见的原因。使用page.on(‘request’, …)打印出实际发出的请求URL与你route()中写的模式仔细对比。注意协议http/https、域名、路径末尾的斜杠。检查点2拦截注册时机。必须在页面发起导航page.goto或触发请求的操作之前调用page.route()。通常建议在page.goto之前就设置好。检查点3Handler函数是否正确调用 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异步APIplaywright.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(status200, bodyMocked) else: await route.continue_() await page.route(**/api/test, async_handler)6. 性能考量与最佳实践滥用请求拦截也会带来性能开销。遵循以下实践可以让你的脚本既强大又高效。精确匹配避免全局拦截尽量不要使用page.route(‘**/*’, handler)。这会强制 Playwright 检查每一个请求包括 favicon.ico、各种像素跟踪等造成不必要的开销。尽量使用具体的 URL 模式或正则表达式。在Context级别设置通用规则如果多个页面都需要相同的 Mock如认证头、资源屏蔽使用context.route()而不是在每个page上重复设置。及时清理如果一个 Mock 只针对单个测试用例在用例结束后使用page.unroute(pattern, handler)将其移除防止污染后续测试。优先使用route.fulfill而非route.fetch() modify如果目的是纯模拟直接构造响应数据比先请求再修改要快得多且不依赖网络。Mock数据的轻量化返回的 Mock 响应体不要过大尤其是模拟列表数据时够用即可。巨大的 JSON 响应会消耗内存和解析时间。结合录制功能生成MockPlaywright Codegen 或 Trace Viewer 可以记录网络请求。你可以先运行一次真实流程从记录中复制真实的请求和响应数据稍作修改作为你的 Mock 数据这样既真实又快捷。网络请求拦截与模拟是 Playwright 从中级迈向高级使用的标志性功能。它赋予了你对测试环境的终极控制权将脆弱的、依赖外部服务的测试转变为稳定、快速、可重复的可靠资产。从简单的请求修改到复杂的动态 Mock 服务这项技术的应用边界只取决于你的想象力。花时间掌握它你构建的自动化解决方案的稳健性和专业性将提升一个数量级。