如何在 Jest 中 Mock fetch 和 Axios 测试 API 调用?
核心思路
测试 API 调用的关键原则是隔离外部依赖——不发出真实网络请求,用 Mock 替代,验证的是"你的代码如何调用 API、如何处理响应",而非 API 本身的行为。
Jest 提供了三种主要 Mock 手段:jest.mock() 模块级替换、jest.spyOn() 方法级监听、jest.fn() 手动创建假函数。理解三者的区别和适用场景,是这道题的答题主线。
Mock Axios 的两种方式
方式一:jest.mock() 替换整个模块
jest.mock('axios') 会将 axios 模块中所有导出替换为 jest.fn(),适合需要完全控制模块行为的场景:
javascriptimport axios from 'axios'; import { getUser } from './api'; jest.mock('axios'); test('getUser 应返回用户数据', async () => { const mockData = { id: 1, name: 'Tom' }; axios.get.mockResolvedValue({ data: mockData }); const result = await getUser(1); expect(result).toEqual(mockData); expect(axios.get).toHaveBeenCalledWith('/users/1'); });
mockResolvedValue 让 axios.get 返回一个 resolved Promise,模拟成功响应。toHaveBeenCalledWith 断言调用参数,确保请求地址正确。
方式二:jest.spyOn() 监听原方法
jest.spyOn 不替换模块,而是包装原方法,可以追踪调用并控制返回值,还能通过 mockRestore() 恢复原实现:
javascriptimport axios from 'axios'; import { getUser } from './api'; test('getUser 应返回用户数据', async () => { const spy = jest.spyOn(axios, 'get').mockResolvedValue({ data: { id: 1, name: 'Tom' } }); const result = await getUser(1); expect(result).toEqual({ id: 1, name: 'Tom' }); spy.mockRestore(); // 恢复 axios.get 原实现 });
何时选哪个? jest.mock() 适合整个测试文件都需要 mock 的场景;jest.spyOn() 适合只想在单个测试中临时 mock、其余测试保留真实行为的场景。
Mock fetch 的两种方式
方式一:jest.fn() 替换全局 fetch
fetch 是全局对象上的方法,直接赋值即可替换:
javascriptimport { fetchPosts } from './api'; beforeEach(() => { global.fetch = jest.fn(() => Promise.resolve({ ok: true, json: () => Promise.resolve([{ id: 1, title: 'Hello' }]), }) ); }); afterEach(() => { jest.restoreAllMocks(); }); test('fetchPosts 应返回帖子列表', async () => { const posts = await fetchPosts(); expect(posts).toEqual([{ id: 1, title: 'Hello' }]); expect(global.fetch).toHaveBeenCalledWith('/api/posts'); });
这里用 beforeEach / afterEach 管理 Mock 生命周期,避免测试间互相污染——这是面试中经常追问的考点。
方式二:jest.spyOn() 监听全局 fetch
javascripttest('fetchPosts 处理响应数据', async () => { jest.spyOn(global, 'fetch').mockResolvedValue({ ok: true, json: () => Promise.resolve([{ id: 1 }]), }); const posts = await fetchPosts(); expect(posts).toEqual([{ id: 1 }]); });
测试错误场景
只测成功路径是不够的,面试官一定会问"网络请求失败了怎么办":
javascripttest('getUser 应抛出网络错误', async () => { axios.get.mockRejectedValue(new Error('Network Error')); await expect(getUser(1)).rejects.toThrow('Network Error'); }); test('getUser 应处理 404 响应', async () => { axios.get.mockRejectedValue({ response: { status: 404, data: { message: 'Not Found' } }, }); await expect(getUser(999)).rejects.toMatchObject({ response: { status: 404 }, }); });
mockRejectedValue 模拟 Promise reject,覆盖网络异常和服务端错误两种情况。
使用 MSW 做更真实的拦截
当项目有大量 API 需要测试时,逐个 jest.mock 维护成本高。MSW(Mock Service Worker)在网络层拦截请求,不需要修改业务代码:
javascriptimport { rest } from 'msw'; import { setupServer } from 'msw/node'; const server = setupServer( rest.get('/api/users/:id', (req, res, ctx) => { return res(ctx.json({ id: req.params.id, name: 'Tom' })); }) ); beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); test('getUser 通过 MSW 返回数据', async () => { const user = await getUser(1); expect(user).toEqual({ id: '1', name: 'Tom' }); });
MSW 的优势:可以在运行时动态修改响应(server.use()),测试超时、限流等边界场景;同一套 handler 可复用于单元测试和集成测试。
关键差异速查
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| Mock 整个第三方库 | jest.mock() | 一键替换所有导出 |
| 单个测试临时 Mock | jest.spyOn() | 可恢复,不影响其他测试 |
| Mock 全局 API(fetch) | jest.fn() / spyOn | fetch 是全局变量,需手动处理 |
| 大量 API 集成测试 | MSW | 网络层拦截,维护成本低 |
面试追问方向
- jest.mock 和 jest.spyOn 的本质区别? mock 是替换,spyOn 是包装。mock 后原实现丢失,spyOn 可恢复。
- 为什么要避免测试中发出真实请求? 网络不稳定、速度慢、可能产生脏数据、依赖外部服务可用性。
- Mock 污染怎么解决? beforeEach 重置、afterEach 调用
jest.restoreAllMocks()、每个测试独立设置数据。 - 如何测试请求重试逻辑? 用
mockRejectedValueOnce连续返回失败,最后一次返回成功,模拟重试后恢复。