第 8 章 · 网络拦截与 Mock
本章目标:学会用
page.route拦截任意网络请求,掌握 fulfill / continue_ / abort 三种处理方式,并能用expect_response精确等待特定 API 响应。
8.1 为什么 E2E 测试需要 Mock 网络
真实后端是 E2E 不稳定的主要来源:数据天天在变、第三方服务偶尔超时、边界场景(余额不足、支付失败)根本无法按需制造。Playwright 的网络拦截让你在浏览器层面接管请求——页面代码毫无感知,测试却能获得完全可控的数据。
from playwright.sync_api import sync_playwright, Route
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
def handle(route: Route):
# 完全不发出真实请求,直接用 mock 数据响应
route.fulfill(json=[{"name": "Strawberry", "id": 21}])
page.route("**/api/v1/fruits", handle) # 拦截匹配的请求
page.goto("https://demo.playwright.dev/api-mocking")
from playwright.sync_api import expect
expect(page.get_by_text("Strawberry")).to_be_visible()
browser.close()URL 匹配支持三种写法:glob 字符串(**/api/v1/**)、正则对象(re.compile(r".*\/api\/v1\/fruits"))和 predicate 函数(接收 route.request 返回布尔值,可按方法、头信息精细过滤)。
8.2 三种处理方式:fulfill / continue_ / abort
拿到 route 后有三条路,覆盖了 Mock 测试的全部需求:
def handle(route: Route):
req = route.request
if req.method == "GET" and "banner" in req.url:
# ① 直接伪造响应:请求根本不会到达服务器
route.fulfill(status=200, json={"img": "/fake.png"})
elif req.method == "POST":
# ② 放行但篡改:补一个自定义请求头再继续发送
headers = {**req.headers, "x-test-mark": "e2e"}
route.continue_(headers=headers)
else:
# ③ 模拟网络失败:用于测试前端的错误处理 UI
route.abort("failed")route.continue_() 还能改 URL、method、postData;route.abort() 的参数是可选的错误码(如 "connectionrefused")。三者选一即可——对同一个 route 既 fulfill 又 continue 会抛异常。
注意 handler 必须有出口
每个被拦截的请求最终必须被 fulfill、continue_ 或 abort 之一处理。如果 handler 里条件分支漏掉了某种情况,对应请求会永远挂起,页面表现为"一直转圈",这是新手最常见的坑。
8.3 修改真实响应:fetch + fulfill 组合
更多时候你不想凭空造数据,而是想让真实 API 跑一遍、只修补结果(比如往列表里塞一条边界数据):
def handle(route: Route):
response = route.fetch() # 真实执行这次请求
data = response.json()
data.append({"name": "Loquat", "id": 100}) # 在真实数据上打补丁
route.fulfill(response=response, json=data) # 用修补后的内容响应
page.route("**/api/v1/fruits", handle)
page.goto("https://demo.playwright.dev/api-mocking")
expect(page.get_by_text("Loquat", exact=True)).to_be_visible()这个模式非常适合测试"分页最后一页""列表为空""超长文本截断"这类依赖后端数据的场景。
8.4 等待与次数控制
等待特定响应:点击按钮触发了 XHR,断言又依赖它的结果时,用 expect_response 包裹动作:
# glob 匹配目标接口
with page.expect_response("**/api/fetch_data") as response_info:
page.get_by_text("Update").click()
response = response_info.value
assert response.status == 200
assert response.json()["ok"] is True限制路由生效次数:page.route 从 v1.15 起支持 times 参数,让 mock 只命中前 N 次,之后的请求正常放行:
# 只有第一次请求返回失败,重试时走真实逻辑
page.route("**/api/payment", lambda r: r.abort(), times=1)
page.goto("https://myshop.example/checkout")配合 page.unroute("**/api/payment") 可以在测试中途撤销拦截,两者组合足以表达绝大多数时序相关的网络场景。
8.5 实战:Mock 第三方支付网关
把前面所有技巧串起来——测试下单流程,但绝不让它真的碰支付服务:
import re
def test_checkout_with_mocked_payment(page):
# 拦支付 SDK 脚本,避免加载外部 JS
page.route(re.compile(r"https://pay-gateway\.example\.com/.*"),
lambda r: r.fulfill(status=200, body="window.PayStubbed=true"))
# 拦支付结果轮询接口,第一次"处理中",第二次"成功"
states = iter([{"state": "pending"}, {"state": "paid"}])
def poll(route):
route.fulfill(json=next(states))
page.route("**/api/pay/status", poll)
page.goto("https://myshop.example/checkout")
page.get_by_role("button", name="去支付").click()
expect(page.get_by_text("支付成功")).to_be_visible(timeout=10_000)本章小结
page.route(url, handler)在浏览器层拦截请求,支持 glob / 正则 / predicate 三种匹配;fulfill伪造响应、continue_放行或改写、abort模拟故障,handler 必须三选一收尾;route.fetch()+fulfill(response=response, ...)实现"真实请求 + 响应修补";expect_response解决"断言依赖异步响应"的竞态;times=N控制拦截生效次数。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. route 处理函数中,既想拿真实接口的数据又想追加一条记录,正确做法是?
2. 如果 route 处理函数的某个条件分支没有调用 fulfill/continue_/abort,会发生什么?
3. page.route 的 times 参数作用是什么?
4. 点击按钮触发 XHR,之后要断言响应数据渲染的结果,应该怎么做?
🛠️ 动手实践
- 为
https://demo.playwright.dev/api-mocking编写测试:mock fruits 接口返回空数组,断言页面出现"空列表"类提示。 - 用
route.fetch()+fulfill把某商品价格改成 0.01 元,走完加购流程验证订单金额。 - 用
route.abort()模拟静态图片全部加载失败,断言页面的降级占位图可见。