很多前端测试失败,不是业务逻辑本身有问题,而是对 fetch 的模拟只做了“替换函数”这一步,却没有把返回结构、作用域和恢复时机处理完整。下面按 Jest 中最常见的几种场景展开,帮助你判断什么时候直接 Mock 全局 fetch 就够了,什么时候需要补齐 Response、环境适配或换成请求拦截方案。
直接 Mock 全局 fetch:先能控,再谈稳定
单元测试里 Mock 全局 fetch 的核心,是把 window.fetch 或 global.fetch 替换成可控的函数,并且不要把这个替换泄漏到其他测试。对 Jest 来说,最直接的方式就是手动挂一个 mock 函数。

常见做法是在测试文件顶部,或者放进 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 也必须保持这个形状。

例如下面这种写法:
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,通常要同时满足四件事:先在正确作用域里替换、再按真实调用链返回、测试后恢复现场、最后验证调用参数。只做到其中一两步,测试可能也会通过,但稳定性和可信度都不够。













