Axios 测试实战指南:模块级 Mock、AxiosError 构造与 axios-mock-adapter 集成测试
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
本文以 Axios 官方测试指南 为核心,系统讲解如何测试依赖 axios 发起 HTTP 请求的业务代码:先用 Vitest/Jest 对 axios 模块本身做 Mock,再演示如何手动构造AxiosError覆盖错误分支,以及如何使用axios-mock-adapter在适配层做更贴近集成场景的模拟。读完本文,你能够编写不依赖真实网络、可重复运行且覆盖成功/失败/超时等路径的 axios 请求测试。
为什么推荐 Mock axios 本身
测试涉及 HTTP 请求的代码,推荐做法是直接模拟(mock)axios 模块,让测试完全不触碰真实网络。这样做的好处是:
- 测试执行快、结果确定,不受网络抖动和外部服务状态影响;
- 你可以精确控制每个方法返回什么(数据、状态、异常),从而逐一覆盖业务代码的分支;
- 与 axios 自身的测试栈一致——该仓库主测试框架即 Vitest(见 package.json 中
"test": "npm run test:vitest",对应脚本vitest run),业务侧用同一套工具链迁移成本最低。
使用 Vitest 或 Jest 进行模块级模拟
Vitest 与 Jest 都支持模块级模拟(vi.mock/jest.mock)。你可以把整个 axios 模块替换为 mock,并控制每个方法的返回值。
被测业务代码示例:
// user-service.js import axios from "axios"; export async function getUser(id) { const { data } = await axios.get(`/api/users/${id}`); return data; }对应的测试文件:
// 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" }; // 让 axios.get 以我们的假响应 resolve 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", async () => { axios.get.mockRejectedValueOnce(new Error("Network error")); await expect(getUser(1)).rejects.toThrow("Network error"); }); });几个要点:
vi.mock("axios")会自动把axios.get、axios.post等导出方法替换为 mock 函数,因此可以直接调用axios.get.mockResolvedValueOnce(...)等断言/打桩 API;- 第一个用例验证了「返回值」与「调用参数」两个可观测行为:
result应等于data,且axios.get收到了拼接好的 URL/api/users/1; - 第二个用例通过
mockRejectedValueOnce模拟请求失败,验证业务函数会把拒绝正确传播出去。
手动构造 AxiosError 覆盖错误处理分支
如果你的业务代码会检查error.response(比如根据error.response.status做 404 降级、根据error.code判断超时),可以直接new一个AxiosError实例,而不必依赖 mock 库:
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);结合源码可以确认这种手写错误对象与真实运行时抛出的错误在结构上是一致的:
- 构造函数签名为
constructor(message, code, config, request, response),见 lib/core/AxiosError.js。传入response后,实例会自动获得this.response = response和this.status = response.status,也就是说上面的例子中mockError.status就是404,业务里error.status === 404的分支可以被精确命中; - 每个
AxiosError都带有isAxiosError = true标记,配合 lib/helpers/isAxiosError.js 可以在 catch 中区分 axios 错误与其他异常; - 常用错误码在源码中定义为静态属性(如
ERR_BAD_REQUEST、ECONNABORTED、ETIMEDOUT、ERR_NETWORK、ERR_CANCELED、ERR_BAD_RESPONSE等),同样位于 lib/core/AxiosError.js 的类定义之后。构造模拟错误时优先从这套常量里选 code,可以让测试数据与真实行为保持同一套词汇表; - 若模拟的是网络层失败(没有
response),可以省略后三个参数,只传message与code(例如ETIMEDOUT),此时mockError.response为undefined,正好用于覆盖if (error.response)的 falsy 分支。
使用 axios-mock-adapter 在适配层模拟
axios-mock-adapter 是一个第三方库,它会往你的 axios 实例上安装一个自定义适配器,在适配层拦截请求。关键区别在于:请求仍然走完整的 axios 管线——你的拦截器依然会执行——因此它更适合集成测试场景。
安装:
npm install --save-dev axios-mock-adapter基本用法:
import axios from "axios"; import MockAdapter from "axios-mock-adapter"; const mock = new MockAdapter(axios); // 模拟一个 GET 请求 mock.onGet("/api/users/1").reply(200, { id: 1, name: "Jay" }); // 模拟一个 POST 请求 mock.onPost("/api/users").reply(201, { id: 2, name: "New User" }); // 模拟网络错误 mock.onGet("/api/failing").networkError(); // 模拟请求超时 mock.onGet("/api/slow").timeout();在测试之间重置模拟,避免用例互相污染:
afterEach(() => { mock.reset(); // 清除所有已注册的处理函数 });为什么"拦截器仍然执行"?从源码结构看:lib/core/dispatchRequest.js 中通过adapters.getAdapter(config.adapter || defaults.adapter, config)解析适配器,而 lib/adapters/adapters.js 的getAdapter同时接受适配器名称和自定义函数(isResolvedHandle判断函数、null、false)。请求拦截器在派发前就已把 config 加工完毕(请求拦截 →dispatchRequest→ 适配器 → 响应拦截),axios-mock-adapter只是替换了管线末端这个可插拔的适配器函数,前后两段的拦截器逻辑完全不受影响。仓库自带的 tests/unit/adapters/adapters.test.js 也验证了「按函数句柄加载自定义适配器」这一能力。
隔离测试拦截器
要单独测试某个拦截器(例如统一注入鉴权头),应在测试里创建新的 axios 实例,而不是复用全局共享实例:
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); // 添加你的拦截器 instance.interceptors.request.use((config) => { config.headers.set("Authorization", "Bearer test-token"); return config; }); // 通过检查 mock 收到的内容来捕获请求配置 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"); }); });两个实现层面的依据:
- 每个 axios 实例在构造时都会创建自己独立的
interceptors.request/interceptors.response两个InterceptorManager,见 lib/core/Axios.js 的构造函数。因此axios.create()出来的实例不会继承默认实例上别人注册的拦截器,天然隔离;InterceptorManager通过use/eject/clear管理拦截器栈,见 lib/core/InterceptorManager.js; axios-mock-adapter绑定在该新实例上后,reply的回调参数就是流经请求拦截器之后的最终 config,把它捕获下来即可断言拦截器的副作用(如上面的Authorization头)。axios v1 的config.headers是AxiosHeaders对象,示例中用config.headers.set(...)写入,正对应 lib/core/AxiosHeaders.js 提供的 API。
实践建议
官方文档给出的三条经验规则:
- 始终在模块级别做模拟(或直接用
MockAdapter)——避免在共享实例上逐个 mock 方法,因为状态可能在测试之间泄漏; - 优先使用
mockResolvedValueOnce/mockRejectedValueOnce而非mockResolvedValue,这样每个用例只消费一次打桩,测试之间互不干扰; - 测试重试逻辑时用
MockAdapter,这样被测的拦截器在每一次重试尝试中都会真实执行,而不是被模块级 mock 短路掉。
延伸:axios 仓库自身的测试组织
如果想参考一个成熟 HTTP 客户端如何组织测试,本仓库的 tests/ 目录值得一看(说明见 tests/README.md):
tests/unit/:聚焦单元行为,网络相关测试通过 tests/setup/server.js 启动本地测试服务器(startHTTPServer/stopHTTPServer),并用try/finally保证清理;tests/browser/:浏览器运行时行为,采用文件内MockXMLHttpRequest风格的手写 mock,在beforeEach替换全局 XHR、afterEach恢复(可参考 tests/browser/adapter.browser.test.js);tests/smoke/esm与tests/smoke/cjs:ESM/CJS 两套打包格式的对齐冒烟测试,验证导入与关键请求流程;- 文件命名约定:单元测试
*.test.js、浏览器测试*.browser.test.js、冒烟测试*.smoke.test.js/*.smoke.test.cjs。
其中「浏览器测试用手写 XHR mock 替换全局对象并在清理钩子中恢复」的思路,与本文推荐的"在共享实例上不留 mock 残留"的原则完全一致,可以直接借鉴到自己的测试基建中。
【免费下载链接】axiosPromise based HTTP client for the browser and node.js项目地址: https://gitcode.com/GitHub_Trending/ax/axios
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考