JS/TS中Jest无法mock自定义Axios类导致单元测试超时
Jest单测Mock单例模式Axios封装类解决方案
测试超时的核心原因是mock逻辑没有命中实际调用的ApiClient单例,请求走了真实网络逻辑。以下是两种可直接落地的实现方案,优先选第一种。
方案1:直接Mock自定义ApiClient模块(推荐)
业务代码中所有请求最终都通过ApiClient.getInstance()拿到的实例发送,直接mock整个ApiClient模块,完全绕开真实实例初始化、拦截器、重试逻辑,不会触发额外依赖调用,稳定性最高。
// 测试文件顶部声明mock,替换为你项目中ApiClient的实际路径 jest.mock('@/utils/ApiClient', () => ({ ApiClient: { getInstance: jest.fn() } })); // 导入依赖,jest.mock会自动提升到导入语句前执行,无需担心顺序问题 import { ApiClient } from '@/utils/ApiClient'; import { dateFilters } from '@/services/kusto'; import { Workload } from '@/constants/workload'; // 自定义mock返回数据 const mockFetchDateOptionsResponse = { data: [/* 你的测试预期返回内容 */] }; describe("dateFilters()", () => { const mockPost = jest.fn(); beforeEach(() => { jest.resetAllMocks(); // 每次用例执行前重置getInstance返回值,注入mock的post方法 (ApiClient.getInstance as jest.Mock).mockReturnValue({ post: mockPost }); }); it("Mock Fetch API for Date Options Response", async () => { // 注入mock响应 mockPost.mockResolvedValue(mockFetchDateOptionsResponse); const response = await dateFilters(Workload.WIN32); // 校验调用逻辑 expect(mockPost).toHaveBeenCalledTimes(1); // 可选:校验请求参数是否符合预期 expect(mockPost).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ db: expect.any(String), csl: expect.any(String) }), expect.objectContaining({ headers: expect.objectContaining({ "x-ms-kql-queryName": "win32DateFilters" }), timeout: expect.any(Number) }) ); expect(response).toEqual(mockFetchDateOptionsResponse); }); });
方案2:Mock底层axios模块(适合需要验证客户端配置的场景)
如果单测需要覆盖ApiClient本身的拦截器、重试配置逻辑,不想直接mock掉整个ApiClient类,可以从底层mock axios的创建逻辑,注意必须手动重置单例的静态缓存。
// 顶部mock axios和axios-retry依赖 jest.mock('axios', () => ({ __esModule: true, default: { create: jest.fn() } })); jest.mock('axios-retry', () => ({ __esModule: true, default: jest.fn() })); // 单独mock拦截器依赖的getKustoToken,避免拦截器执行时走真实逻辑 jest.mock('@/utils/auth', () => ({ getKustoToken: jest.fn().mockResolvedValue('mock-token') })); import Axios from 'axios'; import { ApiClient } from '@/utils/ApiClient'; import { dateFilters } from '@/services/kusto'; import { Workload } from '@/constants/workload'; const mockFetchDateOptionsResponse = { data: [/* 测试预期返回 */] }; describe("dateFilters()", () => { const mockPost = jest.fn(); const mockRequestUse = jest.fn(); const mockResponseUse = jest.fn(); beforeEach(() => { jest.resetAllMocks(); // 核心:清空单例静态缓存,避免上一个用例的实例污染当前测试 (ApiClient as any).instance = undefined; // 给axios.create注入mock实例 (Axios.create as jest.Mock).mockReturnValue({ post: mockPost, interceptors: { request: { use: mockRequestUse }, response: { use: mockResponseUse } } }); mockPost.mockResolvedValue(mockFetchDateOptionsResponse); }); it("Mock Fetch API for Date Options Response", async () => { const response = await dateFilters(Workload.WIN32); expect(mockPost).toHaveBeenCalledTimes(1); expect(response).toEqual(mockFetchDateOptionsResponse); }); });
避坑说明
- 不要通过增加
jest.setTimeout超时时间解决这类报错:超时本质是mock未生效走了真实网络,加超时只会延长失败等待时间,无法解决根本问题。 - 单例模式的静态属性缓存不会被
jest.resetAllMocks自动清空,只要测试涉及直接操作ApiClient类,必须在beforeEach中手动将静态instance属性置空,避免跨用例污染。 - 如果选择方案2,所有拦截器、插件中引用的外部依赖(比如getKustoToken)都需要单独mock,否则执行到对应逻辑时还是会触发真实调用报错。
内容的提问来源于stack exchange,提问作者etotientz
相关产品推荐
相关产品推荐

