首页
/ Axios 测试指南:模块级 Mock、AxiosError 构造与适配器级拦截的完整实战方案

Axios 测试指南:模块级 Mock、AxiosError 构造与适配器级拦截的完整实战方案

2026-09-04 21:49:49作者:韦蓉瑛

本文基于 axios 仓库官方文档 testing.md 编写,系统讲解如何为使用 axios 发起 HTTP 请求的业务代码编写可靠测试:从 Vitest/Jest 的模块级模拟、AxiosError 手工构造,到基于 axios-mock-adapter 的适配器级集成测试与拦截器隔离验证,并结合仓库源码说明每种方案在请求链路中的作用位置与差异,帮助读者在完全脱离真实网络的前提下覆盖成功、失败与错误处理等全部代码路径。

为什么推荐直接模拟 axios 模块

测试依赖 HTTP 请求的代码,最稳妥的思路是让测试不接触真实网络,由测试代码完全掌控被测函数收到的响应。官方文档给出的推荐做法正是“模拟(mock)axios 本身”——用 Vitest 的 vi.mock 或 Jest 的 jest.mock 替换整个 axios 模块,逐个控制各方法的返回值与拒绝原因。

这样做的直接收益是:测试执行确定、速度快,且不会出现因网络抖动、服务不可用导致的 flaky test。需要注意的是,模块级模拟是在模块边界生效的,它绕过了 axios 内部的拦截器链、配置合并与适配器调度逻辑,因此更适合单元级测试;需要验证拦截器与请求全链路行为时,应改用后文的适配器级方案。

用 Vitest 或 Jest 做模块级模拟

下面先看被测的业务模块,它直接使用 axios 默认的 get 方法:

// user-service.js
import axios from "axios";

export async function getUser(id) {
  const { data } = await axios.get(`/api/users/${id}`);
  return data;
}

对应的测试文件用 vi.mock("axios") 对整个模块建立自动模拟,然后分别覆盖成功与失败两条路径:

// user-service.test.js
import { describe, it, expect, vi } from "vitest";
import axios from "axios";
import { getUser } from "./user-service";

vi.mock("axios");

describe("getUser", () => {
  it("returns user data on success", async () => {
    const mockUser = { id: 1, name: "Jay" };

    // Make axios.get resolve with our fake response
    axios.get.mockResolvedValueOnce({ data: mockUser });

    const result = await getUser(1);

    expect(result).toEqual(mockUser);
    expect(axios.get).toHaveBeenCalledWith("/api/users/1");
  });

  it("throws when the request fails", () => {
    axios.get.mockRejectedValueOnce(new Error("Network error"));

    return expect(getUser(1)).rejects.toThrow("Network error");
  });
});

几个要点:

  • vi.mock("axios") 必须写在测试文件顶层(会被测试框架提升到 import 之前执行),它会用自动 mock 替换整个模块,使得被测代码 import axios from "axios" 拿到的不再是真实实现,而是每个方法都是 mock 函数的桩对象;
  • 断言 expect(axios.get).toHaveBeenCalledWith("/api/users/1") 验证的是请求 URL 的拼接逻辑,这在模块级模拟中依然有效,因为它只依赖被测函数自身如何调用 axios,而不依赖 axios 内部行为;
  • 失败路径用 mockRejectedValueOnce 注入一个普通 Error,验证业务代码对“请求被拒绝”的传播是否正确。

模拟 AxiosError:构造带 response 的错误实例

很多业务代码的错误处理分支会检查 error.response.statuserror.response.data(例如 404 时跳转登录、读取服务端错误消息)。要覆盖这些分支,直接抛一个普通 Error 是不够的,需要构造一个带 responseAxiosError 实例:

import axios, { AxiosError } from "axios";
import { vi } from "vitest";

const mockError = new AxiosError(
  "Not Found",
  "ERR_BAD_REQUEST",
  {},       // config
  {},       // request
  {         // response
    status: 404,
    statusText: "Not Found",
    data: { message: "User not found" },
    headers: {},
    config: {},
  }
);

axios.get.mockRejectedValueOnce(mockError);

从源码看,这个构造方式是可靠的。AxiosError 的构造函数签名为 (message, code, config, request, response),见 AxiosError.js

constructor(message, code, config, request, response) {
  super(message);
  // ...
  this.name = 'AxiosError';
  this.isAxiosError = true;
  code && (this.code = code);
  config && (this.config = config);
  request && (this.request = request);
  if (response) {
    this.response = response;
    this.status = response.status;   // status 从 response 自动推导
  }
}

因此手工构造时只要传入 response.status = 404,实例上的 error.status 就会自动为 404,与真实请求失败时的行为一致;error.isAxiosError === true 也成立,业务代码中用 axios.isAxiosError(error) 的守卫同样能命中。

关于第二个参数 code,axios 在 AxiosError.js 中定义了一组静态错误码常量:ERR_BAD_REQUESTERR_BAD_RESPONSEECONNABORTEDETIMEDOUTECONNREFUSEDERR_NETWORKERR_CANCELEDERR_NOT_SUPPORT 等。真实场景中这些码由底层产生——例如状态码判断逻辑在 settle.js 中:4xx 状态会抛出 ERR_BAD_REQUEST,其余非成功状态抛出 ERR_BAD_RESPONSE

reject(new AxiosError(
  'Request failed with status code ' + response.status,
  response.status >= 400 && response.status < 500 ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE,
  response.config, response.request, response
));

编写测试时按同样的语义选用错误码(404 用 ERR_BAD_REQUEST、500 用 ERR_BAD_RESPONSE、网络不可达用 ERR_NETWORK、超时用 ETIMEDOUT),可以让 mock 出的错误与真实运行时的错误在 code 维度上保持一致。

使用 axios-mock-adapter 做适配器级模拟

axios-mock-adapter 是另一种思路更贴近集成的方案:它不是替换模块,而是把自定义 adapter 安装到你的 axios 实例上,在适配器层拦截请求。axios 的请求分发链路是:请求拦截器 → dispatchRequestadapters.getAdapter(...) 解析适配器 → 适配器真正发起请求(见 dispatchRequest.js)。从 dispatchRequest.js 第 52 行 可以看到:

const adapter = adapters.getAdapter(config.adapter || defaults.adapter, config);
return adapter(config).then(/* ... */);

getAdapter 允许传入函数形式的适配器(见 adapters.jsisResolvedHandle 的判断),这正是 mock-adapter 的安装点。由于模拟发生在拦截器链之后,请求/响应拦截器仍会真实执行,因此该方案更适合集成测试——既能验证拦截器逻辑,又完全避免了真实网络。

安装与基本用法:

npm install --save-dev axios-mock-adapter
import axios from "axios";
import MockAdapter from "axios-mock-adapter";

const mock = new MockAdapter(axios);

// Mock a GET request
mock.onGet("/api/users/1").reply(200, { id: 1, name: "Jay" });

// Mock a POST request
mock.onPost("/api/users").reply(201, { id: 2, name: "New User" });

// Mock a network error
mock.onGet("/api/failing").networkError();

// Mock a timeout
mock.onGet("/api/slow").timeout();

四种典型场景一览:

场景 API 说明
正常响应 mock.onGet(url).reply(200, data) 指定状态码与响应体,走完整链路后 resolve
请求体校验 mock.onPost(url).reply(...) 可按方法与 URL 注册不同处理器
网络错误 mock.onGet(url).networkError() 模拟 ERR_NETWORK 类失败
超时 mock.onGet(url).timeout() 模拟超时路径,验证 ETIMEDOUT 处理

为避免各测试之间相互污染,应在每个用例结束后清空已注册的处理规则:

afterEach(() => {
  mock.reset(); // clear all registered handlers
});

需要提醒的是,new MockAdapter(axios) 若作用于默认实例,同样存在共享状态问题;与下一节一样,最佳实践是在每个测试中为模拟绑定独立实例。

隔离测试拦截器

官方文档建议:要单独验证某个拦截器的行为,就在测试里新建一个 axios 实例。从源码看这个建议的合理性:axios.create() 通过 createInstance 创建实例,每个实例携带经 mergeConfig 合并后的独立 defaults,并在 Axios 构造函数 中各自持有全新的 interceptors.request / interceptors.response 两个 InterceptorManager,因此实例之间不会共享拦截器栈:

class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig || {};
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager(),
    };
  }
}

完整示例——验证一个在每次请求上附加 Bearer token 的请求拦截器:

import axios from "axios";
import MockAdapter from "axios-mock-adapter";

describe("auth interceptor", () => {
  it("attaches a Bearer token to every request", async () => {
    const instance = axios.create();
    const mock = new MockAdapter(instance);

    // Add your interceptor
    instance.interceptors.request.use((config) => {
      config.headers.set("Authorization", "Bearer test-token");
      return config;
    });

    // Capture the request config by inspecting what mock received
    let capturedConfig;
    mock.onGet("/api/data").reply((config) => {
      capturedConfig = config;
      return [200, {}];
    });

    await instance.get("/api/data");

    expect(capturedConfig.headers["Authorization"]).toBe("Bearer test-token");
  });
});

这段测试的验证原理值得展开:reply 接收回调函数时,参数 config拦截器链执行完毕后到达适配器的最终配置。从 Axios.js 可以确认请求链的组装顺序——请求拦截器先按注册顺序 push 进链上,dispatchRequest 位于链尾,响应拦截器随后——所以适配器层捕获到的 config 已经过全部请求拦截器加工,此时断言其中的 Authorization 头,就等价于断言“拦截器对每个请求生效”。

拦截器注册时还可传入 synchronousrunWhen 选项(见 InterceptorManager.jsuse 方法对 options 的处理),测试条件式拦截器时可以通过 runWhen 构造不同配置来验证跳过/执行的分支。

实战建议汇总

官方文档给出的三条建议,结合源码可以给出更完整的理由:

  1. 始终在模块级别做模拟(或使用 MockAdapter)。避免在共享的默认实例上单独 mock 某个方法:mock 的调用记录与 *Once 队列是实例状态,跨测试文件/用例容易泄漏。vi.mock 按测试文件隔离,MockAdapter 配合 afterEachmock.reset() 清空规则,都能保证状态不逃逸。
  2. 优先使用 mockResolvedValueOnce / mockRejectedValueOnce,而不是不带 Once 的版本。Once 系列每次只消耗一个预设结果,用后即空,测试之间天然隔离;不带 Once 的模拟会持续生效,一旦用例顺序或数量变化就可能误伤其他测试。
  3. 测试重试逻辑时使用 axios-mock-adapter。重试通常由请求/响应拦截器实现,而模块级模拟会直接短路掉 axios 内部链路,被测的重试拦截器根本不会执行;适配器级模拟位于拦截器之后,每次“尝试”都会真实经过拦截器链,重试次数、退避行为等才能被真正观察到。

两种模拟策略的选择

维度 vi.mock("axios") 模块级 axios-mock-adapter 适配器级
拦截点 模块导入边界 getAdapter 处(拦截器链之后)
请求/响应拦截器 不执行 真实执行
配置合并、headers 处理 不执行 真实执行
适用场景 单元测试,只关心业务逻辑 集成测试、拦截器/重试逻辑
状态隔离手段 测试文件级隔离 + *Once afterEachmock.reset()、独立实例

两者并非互斥:单元测试用模块级 mock 快速隔离业务逻辑,涉及拦截器、重试、错误分类的集成行为交给 axios-mock-adapter,即可在零网络依赖下覆盖 axios 应用代码的绝大部分路径。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384