Axios 测试指南:模块级 Mock、AxiosError 构造与适配器级拦截的完整实战方案
本文基于 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.status 或 error.response.data(例如 404 时跳转登录、读取服务端错误消息)。要覆盖这些分支,直接抛一个普通 Error 是不够的,需要构造一个带 response 的 AxiosError 实例:
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_REQUEST、ERR_BAD_RESPONSE、ECONNABORTED、ETIMEDOUT、ECONNREFUSED、ERR_NETWORK、ERR_CANCELED、ERR_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 的请求分发链路是:请求拦截器 → dispatchRequest → adapters.getAdapter(...) 解析适配器 → 适配器真正发起请求(见 dispatchRequest.js)。从 dispatchRequest.js 第 52 行 可以看到:
const adapter = adapters.getAdapter(config.adapter || defaults.adapter, config);
return adapter(config).then(/* ... */);
而 getAdapter 允许传入函数形式的适配器(见 adapters.js 中 isResolvedHandle 的判断),这正是 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 头,就等价于断言“拦截器对每个请求生效”。
拦截器注册时还可传入 synchronous 与 runWhen 选项(见 InterceptorManager.js 中 use 方法对 options 的处理),测试条件式拦截器时可以通过 runWhen 构造不同配置来验证跳过/执行的分支。
实战建议汇总
官方文档给出的三条建议,结合源码可以给出更完整的理由:
- 始终在模块级别做模拟(或使用 MockAdapter)。避免在共享的默认实例上单独 mock 某个方法:mock 的调用记录与
*Once队列是实例状态,跨测试文件/用例容易泄漏。vi.mock按测试文件隔离,MockAdapter配合afterEach中mock.reset()清空规则,都能保证状态不逃逸。 - 优先使用
mockResolvedValueOnce/mockRejectedValueOnce,而不是不带Once的版本。Once系列每次只消耗一个预设结果,用后即空,测试之间天然隔离;不带Once的模拟会持续生效,一旦用例顺序或数量变化就可能误伤其他测试。 - 测试重试逻辑时使用
axios-mock-adapter。重试通常由请求/响应拦截器实现,而模块级模拟会直接短路掉 axios 内部链路,被测的重试拦截器根本不会执行;适配器级模拟位于拦截器之后,每次“尝试”都会真实经过拦截器链,重试次数、退避行为等才能被真正观察到。
两种模拟策略的选择
| 维度 | vi.mock("axios") 模块级 |
axios-mock-adapter 适配器级 |
|---|---|---|
| 拦截点 | 模块导入边界 | getAdapter 处(拦截器链之后) |
| 请求/响应拦截器 | 不执行 | 真实执行 |
| 配置合并、headers 处理 | 不执行 | 真实执行 |
| 适用场景 | 单元测试,只关心业务逻辑 | 集成测试、拦截器/重试逻辑 |
| 状态隔离手段 | 测试文件级隔离 + *Once |
afterEach 中 mock.reset()、独立实例 |
两者并非互斥:单元测试用模块级 mock 快速隔离业务逻辑,涉及拦截器、重试、错误分类的集成行为交给 axios-mock-adapter,即可在零网络依赖下覆盖 axios 应用代码的绝大部分路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00