用 Playwright route 拦截网络请求:测试不依赖后端

2026-08-18

写端到端测试最容易翻车的地方不是页面本身,是页面背后那一堆接口。列表接口今天返回三条明天返回三十条,断言就飘了;要测「空状态」得先去数据库把数据删干净;后端还在开发,前端页面已经能点了,测试却跑不起来。

Playwright 把这件事放在 Route 这个对象上处理。请求还没出浏览器的时候就被截住,你决定它接下来往哪走。下面的内容全部依据仓库里的 docs/src/mock.mddocs/src/network.mddocs/src/api/class-route.md,我们没有跑过任何一段代码,只是把文档和签名里写明的东西转述出来。

前置条件

先把版本和语言绑定这两件事说清楚,这一段跳过去后面会踩坑。

Page.routeRoute.abort / Route.continue / Route.fulfilldocs/src/api/class-route.mdclass-page.md 里都标了 since: v1.8,属于很早就有的能力。但拦截里常用的几个东西是后来才加的,签名文件里逐个标了起始版本:

  • Route.fallbacksince: v1.23
  • Route.fetchsince: v1.29Route.fulfilljson 选项也标 since: v1.29
  • Page.routeFromHARsince: v1.23,它的 updateModeupdateContent 两个选项标 since: v1.32
  • Page.routetimes 选项标 since: v1.15
  • Page.unrouteAllsince: v1.41
  • Route.fetchmaxRetriessince: v1.46

语言绑定的差别更要看清。Route.continueclass-route.md 的方法头下面写着 alias-java: resumealias-python: continue_——也就是说 Java 里这个方法叫 resume,Python 里叫 continue_(末尾有下划线,因为 continue 是 Python 关键字)。Route.fulfilljson 选项标了 langs: js, python, csharp,Java 那一侧没有这个选项,仓库示例里 Java 是先把对象序列化成字符串再 setBody。反过来,bodyBytes 标的是 langs: csharp, java,JS 和 Python 那侧没有。写代码前去对应语言的段落里确认一遍,别拿 JS 的写法往 Python 上套。

匹配规则:glob 不是你熟悉的那个 glob

docs/src/network.md 有专门一节讲 URL 匹配,写明 Playwright 用的是「简化的 glob 模式」,规则只有四条:

  1. 单个 * 匹配除 / 以外的任意字符,双 ** 匹配包括 / 在内的任意字符
  2. 问号 ? 只匹配问号本身,想匹配任意单字符要用 *
  3. 花括号 {} 用来匹配逗号分隔的一组选项
  4. 反斜杠 \ 用来转义特殊字符,转义反斜杠自身要写 \\

第二条是最反直觉的一条。在别处 ? 通常代表「任意一个字符」,这里它就是字面量问号——文档给的例子是 https://example.com/?page=1 匹配 https://example.com/?page=1 但不匹配 https://example.com。第四条对 Windows 用户额外重要:URL glob 里的反斜杠是转义符,而 Windows 的文件路径分隔符也是反斜杠,这两件事在同一段代码里出现时很容易混。HAR 文件路径那种参数是文件系统路径,不走 glob;route() 的第一个参数是 URL 模式,走 glob。

文档在这一节末尾还列了几条要点,其中两条最常用得上:glob 必须匹配整个 URL,不是匹配其中一部分;匹配需求复杂时建议改用正则。Page.route 的参数签名里写明 url 可以是 stringRegExp、或者接收 URL 返回 boolean 的谓词函数(JS 那一侧的签名里另外列了 URLPattern)。

三种处置:fulfill、abort、continue

拦到请求之后,Route 对象上有三条终结路径。

fulfill 是直接造一个响应回去,请求不出浏览器。 docs/src/mock.md 的第一个示例是这样:

await page.route('*/**/api/v1/fruits', async route => {
  const json = [{ name: 'Strawberry', id: 21 }];
  await route.fulfill({ json });
});

Python 同步写法在同一文件里是 route.fulfill(json=json),注册用 page.route("*/**/api/v1/fruits", handle)。这里的 Strawberry21 是仓库示例里的值,不是什么约定俗成的东西,换成你自己的数据即可。class-route.md 写明 json 选项在没显式设置时会把 content type 设成 application/jsonstatus 的默认值是 200(这是仓库当前文档里的默认值,随版本可能变动);还可以用 path 直接拿一个本地文件当响应体,文档说 content type 从文件扩展名推断,相对路径相对当前工作目录解析。

abort 是掐断。 class-route.mdabort 接受一个可选的 errorCode,默认值是 failed(同样是当前文档里的默认值),文档列出了一组可选的错误码,像 connectionrefusedtimedoutnamenotresolved 这类。这个参数的价值在于你可以造出不同种类的网络故障来测前端的降级逻辑,而不是笼统地失败一次。docs/src/network.md 里屏蔽图片的例子是:

await page.route('**/*.{png,jpg,jpeg}', route => route.abort());

continue 是放行,可以顺手改。 它接受 urlmethodpostDataheaders 四个覆盖项。class-route.md 的 Details 一节里有一条很容易忽略的说明:headers 会同时作用于这个请求和它引发的重定向,而 urlmethodpostData 只作用于原始请求,不会带到重定向后的请求上。另有一条 warning 明确写着部分请求头是「forbidden」不能覆盖,举了 CookieHostContent-Length,说如果对这些头提供了覆盖值,覆盖会被忽略、仍然用原始请求头;要设自定义 cookie 得改用 BrowserContext.addCookies

还有第四条路径 fallback,它和 continue 的差别文档说得很直白:continue 会立即把请求发到网络,其它匹配的 handler 不会被调用;fallback 则会让链上下一个匹配的 handler 接着跑。多个 route 匹配同一个请求时,class-page.md 写明「最近注册的 route 优先」,class-route.md 补了一句「运行顺序与注册顺序相反」。另外 page 级别的 route 优先于用 BrowserContext.route 注册的 context 级 route。

如果你要在中间改造请求再交给下一个 handler,fallback 也接受同样的 url / method / postData / headers 覆盖项,但它的 url 选项文档里多了一句:改 URL 不影响 route 匹配,所有 route 都按原始请求 URL 匹配。

想「先真实请求一次再改返回值」用 Route.fetch,它执行请求但不 fulfill,把 APIResponse 交给你,再连同修改后的 body 一起 fulfill

await page.route('*/**/api/v1/fruits', async route => {
  const response = await route.fetch();
  const json = await response.json();
  json.push({ name: 'Loquat', id: 100 });
  await route.fulfill({ response, json });
});

以上代码块均原样取自仓库文档中的示例;如需组合成自己的用法,请以仓库最新代码为准,我们未经实测。

HAR:把一次真实流量存下来反复回放

手写 mock 数据在接口字段多的时候很痛苦。docs/src/mock.md 给的另一条路是 HAR 文件,流程是三步:录一份 HAR、把它和测试一起提交到版本库、测试里用它来回放。

录制和回放用的是同一个方法 Page.routeFromHAR(context 级别是 BrowserContext.routeFromHAR),靠 update 选项切换。update: true 时它不从文件里取响应,而是拿真实网络信息去创建或更新这个 HAR 文件;把 update 关掉或去掉,就变成从文件里回放。url 选项用来限定只有匹配的请求走 HAR,不指定就是所有请求都从 HAR 取。

Python 那一侧写法在同一文件里是 page.route_from_har("./hars/fruit.har", url="*/**/api/v1/fruits", update=True),回放时把 update 改成 False

回放的匹配规则文档写得很细,这几条决定了你的 HAR 会不会「明明录了却匹配不上」:URL 和 HTTP method 是严格匹配;POST 请求连 POST payload 也严格匹配;如果多条录制都匹配,选请求头匹配得最多的那一条;命中一条会导致重定向的记录时,重定向会被自动跟随。另外 HAR 文件名如果以 .zip 结尾,会被当成包含 HAR 和网络负载的归档;你也可以把归档解开、手工编辑负载或 HAR log,再指向解开后的 har 文件,所有负载会相对这个文件的位置解析。

routeFromHARnotFound 选项管的是「HAR 里没这条记录怎么办」:设成 abort 就把请求掐断,设成 fallback 就把请求发到真实网络,文档写明默认是 abort(这是仓库当前文档里的默认值,随版本可能变动)。updateModefullminimal 两个取值,文档说 minimal 只记录回放所必需的信息,会省掉大小、时序、page、cookie、安全等回放时用不到的部分,默认是 minimalupdateContentembedattach 两个取值,分别是把内容内联进 HAR,还是作为单独文件或 ZIP 条目持久化。

mock.md 说仓库推荐用 update 选项来录制,同时也提到可以用 Playwright CLI 录,文档里 JS 那一侧给的命令是:

npx playwright open --save-har=example.har --save-har-glob="**/api/**" https://example.com

Python 那一侧同一节里是不带 npxplaywright open ...,C# 侧是通过 playwright.ps1 调用——这几条在 docs/src/mock.md 里是分开列的,别串。

边界:文档明说不管用的情况

这几条在 class-page.mddocs/src/network.md 里都是以 note 的形式写着的,值得单独抄出来:

  • Service Worker 接管的请求不会被拦到。 Page.route 的说明里明确写了这一点,并建议把 Browser.newContextserviceWorkers 选项设成 'block'network.md 专门有一节叫「Missing Network Events and Service Workers」,还点名了 Mock Service Worker(MSW)这类工具:它自带的 Service Worker 会接管网络请求,于是这些请求对 route 不可见。routeFromHAR 同样受这条影响,文档写明 Playwright 不会从 HAR 里服务被 Service Worker 拦截的请求。
  • popup 页面的第一个请求 Page.route 拦不到,要用 BrowserContext.route
  • 重定向时 handler 只对第一个 URL 调用一次。
  • 启用 routing 会禁用 http cache。 这句在 class-page.md 里就一行 note,但它意味着开了拦截之后的网络行为和平时不完全一样。
  • 匹配到 route 的请求会一直 stall,直到它被 continue、fulfill 或 abort——handler 里漏掉终结调用,请求就挂在那里。

清理方面,Page.unroute 用来移除某条 route,不传 handler 就移除该 URL 上的所有 route;Page.unrouteAll 移除 routerouteFromHAR 建的全部 route,它的 behavior 选项有三个取值:default 不等待正在跑的 handler(handler 抛错可能变成未处理错误)、wait 等待跑完、ignoreErrors 不等待且静默吞掉解除路由后 handler 抛出的错误。这个选项在 docs/src/api/params.md 里标了 langs: js, csharp, python,Java 侧没有列出来。

怎么确认拦对了

最直接的办法是把断言写成「只有 mock 数据存在时才通过」。mock.md 的示例里,mock 完之后跳转页面,断言 page.getByText('Strawberry') 可见——这个字符串本来不可能出现在真实接口返回里,所以断言通过就说明走的是 mock。

另一个办法是看 trace。mock.md 在几处都写明可以从测试的 trace 里看出 API 到底有没有被调用:纯 mock 的那个例子里 API 从未被调用而 route 被 mock 数据 fulfill 了;改响应的例子里 API 被调用了、响应被修改了;HAR 回放的例子里 route 由 HAR 文件 fulfill、API 没被调用。也就是说 trace 能区分「拦住了」和「透传了」这两种情况,不用靠猜。

如果你改的是 HAR,mock.md 提到录出来的内容在 hars 目录下的哈希 .txt 文件里,可以直接打开编辑 JSON,这个文件应当提交进版本库;只要以 update: true 再跑一次,它就会被真实请求覆盖回去——这一点在改完 mock 数据之后尤其要留神,回放阶段记得把 update 关掉。

最后提醒一句:Playwright 会启动真实浏览器、执行页面脚本,拦截本身不构成任何隔离保证;HAR 文件是完整的网络记录,里面可能带着 token、cookie 和真实业务数据,提交进版本库前自己过一遍。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。


本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测, 因此不涉及运行速度、稳定性与实际表现的任何描述。 该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。 本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。