JavaScript 中 Fetch 怎么在单元测试中优雅地 Mock 全局 fetch 函数

宋时安
宋时安
发布时间:2026-08-25 | 58人浏览过
原创

前言

在 JavaScript 单元测试里,Mock `fetch` 看起来只是替换一个全局函数,真正容易出问题的却是返回结构、环境差异和测试收尾。本文按 Jest 的常见场景拆开讲:怎样安全接管 `global.fetch`、什么时候该补 `Response` 调用链,以及 JSDOM、Node、MSW 分别适合什么写法。

很多前端测试失败,不是业务逻辑本身有问题,而是对 fetch 的模拟只做了“替换函数”这一步,却没有把返回结构、作用域和恢复时机处理完整。下面按 Jest 中最常见的几种场景展开,帮助你判断什么时候直接 Mock 全局 fetch 就够了,什么时候需要补齐 Response、环境适配或换成请求拦截方案。

直接 Mock 全局 fetch:先能控,再谈稳定

单元测试里 Mock 全局 fetch 的核心,是把 window.fetch 或 global.fetch 替换成可控的函数,并且不要把这个替换泄漏到其他测试。对 Jest 来说,最直接的方式就是手动挂一个 mock 函数。

全局 fetch Mock 的测试流程示意,包含替换、设定返回值、执行测试和恢复现场。
全局 fetch Mock 的最小闭环先替换全局 fetch,再为单条测试配置返回值,最后恢复现场,才能避免测试之间互相污染。

常见做法是在测试文件顶部,或者放进 beforeAll 中初始化:

global.fetch = jest.fn();

之后在具体测试里,为本次调用设置返回值:

fetch.mockResolvedValue(new Response(JSON.stringify({ ok: true }), { status: 200 }));

这样做的好处很直接:测试不会真的发起网络请求,你可以精确控制状态码和响应内容,也方便覆盖成功、失败、空数据等分支。

但这一步还不算完整。Mock 过的全局函数如果不恢复,下一条测试很可能被前一条残留状态影响,所以结束后要显式清理:

fetch.mockRestore();

这类清理通常放在 afterEach 或 afterAll。如果你的测试会频繁改写返回值,优先放在 afterEach,隔离性更好。

把重复逻辑收进 helper,成功与异常分开写

如果每个测试都手写 mockResolvedValue、JSON.stringify 和 Response 构造,文件会很快变得啰嗦。更实用的做法,是把成功响应封装成一个小工具函数。

function mockFetch(json, status = 200) {
  fetch.mockResolvedValue(new Response(JSON.stringify(json), { status }));
}

测试里直接调用即可:

mockFetch({ data: 'test' }, 201);

这种封装有两个实际价值。第一,测试意图更清楚,读代码时一眼就能看出“这里是在准备一个 201 的 JSON 响应”;第二,后续如果项目里统一要补头信息、调整响应结构,只改一个 helper 就行。

除了成功路径,异常路径也建议单独写清楚,而不是混在同一个 helper 里硬塞参数。比如网络失败时,可以直接这样模拟:

fetch.mockRejectedValue(new Error('Network failed'));

这样测试更接近真实语义:这不是“接口返回错误 JSON”,而是请求 Promise 本身被拒绝。两种情况在业务代码里往往不是同一套处理逻辑,测试时也应该区分。

别只 Mock 一个 Promise:要对齐 fetch 的调用链

fetch 最容易被 Mock 失真的地方,在于它返回的并不是最终 JSON,而是一个 Promise<Response>。而 Response.json() 本身又会再返回一个 Promise。你的业务代码如果写的是链式调用,Mock 也必须保持这个形状。

fetch 返回 Promise<Response> 与 Response.json() 再返回 Promise 的调用链示意。
Mock 要跟上 Response 调用链Mock 不能只停在第一个 Promise,业务代码如果会继续调用。

例如下面这种写法:

await (await fetch(url)).json()

如果只让 fetch 返回一个普通对象,测试就可能在 .json() 这里直接报错。因此更稳妥的模拟方式是,至少把 json() 这一层补出来:

fetch.mockResolvedValue({
  json: jest.fn().mockResolvedValue({ ok: true })
} as unknown as Response);

这段代码的重点,不是类型写法本身,而是返回结构要和真实 Response 一致到“够用”的程度。只要业务里会访问 json()、status、ok 等属性,你的 Mock 就要把这些字段一并补齐,否则测试通过了,也未必说明真实代码可靠。

如果希望更贴近真实对象,现代 Jest 环境下可以直接构造真实的 Response 实例;在 Node 场景中,也可以结合 jest.requireActual('node-fetch') 来拿到更接近运行时的实现。这种方式比手写对象更稳,尤其适合测试里确实依赖 Response 行为的代码。

JSDOM、Node 和 MSW:不同环境要用不同判断

Mock fetch 看起来是同一件事,但在不同测试环境里,起点并不完全一样。这里最容易踩坑的是“当前环境到底有没有原生 fetch”。

JSDOM 环境

在浏览器模拟环境里,通常可以操作 window.fetch,也可以操作 global.fetch。两者很多时候都能工作,但统一写成 global.fetch 往往更稳定,尤其是测试运行器和跨文件 setup 混用时,更不容易出现引用不一致的问题。

Node 环境

Node 场景下,如果运行环境本身没有你需要的 fetch 实现,就要先补上再去 Mock。原文给出的常见方式是先安装 node-fetch,然后手动挂载:

global.fetch = require('node-fetch');

之后再继续做替换或拦截。这里的关键不是必须使用哪一个包,而是要先确认测试运行时是否真的具备 fetch 和 Response。

MSW 适合什么时候

如果你已经在用 msw(Mock Service Worker),那就不一定要直接 Mock fetch 了。MSW 的思路是拦截网络请求,让应用代码继续按正常方式调用接口。它更适合集成测试或 E2E,优点是真实感更强;但对只想验证某个函数输入输出的单元测试来说,通常会偏重。

简单说,单元测试优先考虑直接 Mock 全局 fetch;只有当你需要更完整的请求链路、路由级联动或接口协议模拟时,再考虑 MSW 这类方案。

最后别漏掉断言与清理

把响应模拟出来,只完成了一半。真正能说明业务逻辑正确的,是你是否验证了请求有没有按预期发出。至少要检查调用次数、URL 和参数,而不是只看函数有没有返回数据。

例如:

expect(fetch).toHaveBeenCalledWith('/api/user', expect.any(Object))

这类断言能补上两个常见盲区:一是函数虽然执行了,但请求地址写错;二是请求配置对象缺失了方法、头信息或 body。没有这一步,测试很容易变成“你 Mock 了什么,它就返回什么”,却没有真正约束调用行为。

把整个思路收束一下,单元测试里优雅地 Mock 全局 fetch,通常要同时满足四件事:先在正确作用域里替换、再按真实调用链返回、测试后恢复现场、最后验证调用参数。只做到其中一两步,测试可能也会通过,但稳定性和可信度都不够。

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表