5月28日 06:59

如何在 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(),适合需要完全控制模块行为的场景:

javascript
import 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'); });

mockResolvedValueaxios.get 返回一个 resolved Promise,模拟成功响应。toHaveBeenCalledWith 断言调用参数,确保请求地址正确。

方式二:jest.spyOn() 监听原方法

jest.spyOn 不替换模块,而是包装原方法,可以追踪调用并控制返回值,还能通过 mockRestore() 恢复原实现:

javascript
import 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 是全局对象上的方法,直接赋值即可替换:

javascript
import { 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

javascript
test('fetchPosts 处理响应数据', async () => { jest.spyOn(global, 'fetch').mockResolvedValue({ ok: true, json: () => Promise.resolve([{ id: 1 }]), }); const posts = await fetchPosts(); expect(posts).toEqual([{ id: 1 }]); });

测试错误场景

只测成功路径是不够的,面试官一定会问"网络请求失败了怎么办":

javascript
test('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)在网络层拦截请求,不需要修改业务代码:

javascript
import { 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()一键替换所有导出
单个测试临时 Mockjest.spyOn()可恢复,不影响其他测试
Mock 全局 API(fetch)jest.fn() / spyOnfetch 是全局变量,需手动处理
大量 API 集成测试MSW网络层拦截,维护成本低

面试追问方向

  • jest.mock 和 jest.spyOn 的本质区别? mock 是替换,spyOn 是包装。mock 后原实现丢失,spyOn 可恢复。
  • 为什么要避免测试中发出真实请求? 网络不稳定、速度慢、可能产生脏数据、依赖外部服务可用性。
  • Mock 污染怎么解决? beforeEach 重置、afterEach 调用 jest.restoreAllMocks()、每个测试独立设置数据。
  • 如何测试请求重试逻辑?mockRejectedValueOnce 连续返回失败,最后一次返回成功,模拟重试后恢复。
标签:AxiosJest