axios-mock-adapter vs fetch-mock
前端测试中模拟 HTTP 请求的工具选型
axios-mock-adapterfetch-mock类似的npm包:

前端测试中模拟 HTTP 请求的工具选型

axios-mock-adapterfetch-mock 都是用于在前端测试中拦截和模拟 HTTP 请求的工具库,但它们针对不同的底层网络 API。axios-mock-adapter 专为基于 Axios 的项目设计,通过拦截 Axios 实例的请求实现 mock;而 fetch-mock 则直接拦截原生 fetch API,适用于使用 fetch 或基于 fetch 封装的 HTTP 客户端(如 Ky、undici-fetch 等)的项目。两者都能在单元测试、集成测试或开发环境中提供可控的网络响应,避免依赖真实后端服务。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
axios-mock-adapter969,6793,55267.9 kB961 年前MIT
fetch-mock01,309157 kB95 个月前MIT

axios-mock-adapter vs fetch-mock:前端 HTTP Mock 工具深度对比

在前端测试中,可靠地模拟网络请求是保证测试可重复性和隔离性的关键。axios-mock-adapterfetch-mock 是两个主流方案,但它们服务于不同的 HTTP 客户端生态。本文从实际工程角度出发,对比它们的核心能力、使用方式和适用边界。

🧪 核心定位:适配不同网络层

axios-mock-adapter 仅作用于 Axios 实例。它通过替换 Axios 内部的请求适配器(adapter)来拦截请求,因此必须在创建 Axios 实例后绑定。

// axios-mock-adapter 示例
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

const mock = new MockAdapter(axios);

mock.onGet('/users').reply(200, [{ id: 1, name: 'Alice' }]);

// 此调用会被拦截
axios.get('/users').then(response => console.log(response.data));

fetch-mock 则直接拦截 全局 fetch 函数。无论请求来自你自己的代码还是第三方库,只要使用了 fetch,就能被拦截。

// fetch-mock 示例
import fetchMock from 'fetch-mock';

fetchMock.get('/api/users', { body: [{ id: 1, name: 'Alice' }], status: 200 });

// 此调用会被拦截
fetch('/api/users').then(res => res.json()).then(data => console.log(data));

⚠️ 注意:axios-mock-adapter 无法拦截 fetchfetch-mock 也无法拦截 Axios(除非 Axios 底层使用 fetch,但默认不是)。

🔍 请求匹配能力:路径、方法与高级条件

两者都支持基于 URL 和 HTTP 方法的匹配,但语法和灵活性有所不同。

基础匹配

axios-mock-adapter 使用链式方法指定方法和路径:

mock.onGet('/users').reply(200, users);
mock.onPost('/login').reply(201, { token: 'abc' });

fetch-mock 在第一个参数中组合方法和路径(或使用对象配置):

fetchMock.get('/users', users);
fetchMock.post('/login', { token: 'abc' });

动态路径与正则

两者均支持正则表达式和通配符:

// axios-mock-adapter
mock.onGet(/\/users\/\d+/).reply(200, user);

// fetch-mock
fetchMock.get(/\/api\/users\/\d+/, user);

基于请求体或 Headers 的匹配

axios-mock-adapter 可通过回调函数检查完整请求配置:

mock.onPost('/search', { keyword: 'test' }).reply(200, results);

// 或使用函数判断
mock.onPost('/auth', config => {
  return config.headers['X-API-Key'] === 'secret';
}).reply(200, { valid: true });

fetch-mock 支持更丰富的匹配器(matcher),包括函数形式:

fetchMock.post(
  (url, opts) => {
    return url === '/api/auth' && opts.headers?.['X-API-Key'] === 'secret';
  },
  { valid: true }
);

📤 响应控制:状态码、延迟与动态逻辑

固定响应

两者都支持直接返回状态码和数据:

// axios-mock-adapter
mock.onGet('/data').reply(200, { message: 'ok' });

// fetch-mock
fetchMock.get('/data', { body: { message: 'ok' }, status: 200 });

延迟与错误模拟

axios-mock-adapter 使用 timeout 选项模拟超时,或抛出错误:

mock.onGet('/slow').reply(200, {}, 1000); // 延迟 1 秒
mock.onGet('/error').reply(500);
mock.onGet('/network-error').networkError();

fetch-mock 通过 delaythrows 控制:

fetchMock.get('/slow', { body: {}, delay: 1000 });
fetchMock.get('/error', { status: 500 });
fetchMock.get('/network-error', { throws: new TypeError('Failed to fetch') });

动态响应(基于请求内容)

两者都允许在 reply 函数中访问请求信息:

// axios-mock-adapter
mock.onPost('/echo').reply(config => {
  const data = JSON.parse(config.data);
  return [200, { received: data }];
});

// fetch-mock
fetchMock.post('/echo', (url, opts) => {
  return { body: { received: JSON.parse(opts.body) } };
});

🧹 清理与重置:避免测试污染

良好的测试实践要求每个测试用例结束后清理 mock 状态。

axios-mock-adapter 提供 reset()restore()

beforeEach(() => mock.reset()); // 清除所有 mock 规则
afterAll(() => mock.restore()); // 恢复原始 Axios adapter

fetch-mock 使用 reset()restore()

beforeEach(() => fetchMock.reset());
afterAll(() => fetchMock.restore());

两者行为类似,都能有效防止测试间干扰。

🌐 环境兼容性:浏览器 vs Node.js

  • axios-mock-adapter:由于 Axios 本身跨平台,该库在浏览器和 Node.js 中均可使用,无需额外配置。
  • fetch-mock:原生 fetch 在 Node.js < 18 中不可用。在旧版 Node 环境中,需配合 node-fetchundici 并手动注入全局 fetch,否则会报错。
// 在 Node.js < 18 中使用 fetch-mock 的典型 setup
import fetch from 'node-fetch';
global.fetch = fetch;
import fetchMock from 'fetch-mock';

🛠️ 调试与诊断支持

fetch-mock 提供了更强的调试能力。例如,可以检查哪些请求未被 mock:

fetchMock.catch(); // 未匹配的请求会抛出错误
console.log(fetchMock.calls()); // 查看所有调用记录

axios-mock-adapter 本身不提供调用日志,但可通过 Axios 的拦截器自行记录。

🆚 总结:如何选择?

维度axios-mock-adapterfetch-mock
适用客户端Axios原生 fetch 或基于 fetch 的库
拦截范围仅绑定的 Axios 实例全局 fetch 调用
Node.js 支持开箱即用需 polyfill(Node < 18)
高级匹配支持(通过函数)更灵活(多种 matcher)
调试能力基础较强(调用日志、未匹配捕获)

最终建议

  • 如果你的项目 使用 Axios,毫不犹豫选择 axios-mock-adapter —— 它集成简单、语义清晰,且不会引入无关依赖。
  • 如果你的项目 使用 fetch(无论是原生还是封装),fetch-mock 是更合适的选择,尤其当你需要拦截第三方库发出的 fetch 请求时。
  • 不要混用:在一个项目中同时使用两者通常意味着架构不一致,应统一 HTTP 客户端后再选型。

记住:工具的选择应由你的网络层决定,而不是反过来。先明确你用的是 Axios 还是 fetch,答案自然就清晰了。

如何选择: axios-mock-adapter vs fetch-mock

  • axios-mock-adapter:

    如果你的项目使用 Axios 作为 HTTP 客户端,axios-mock-adapter 是最自然的选择。它与 Axios 深度集成,API 设计贴合 Axios 的请求/响应结构,配置简单直观。特别适合已有 Axios 调用栈、需要精确匹配请求方法、URL、参数或 headers 的测试场景。但请注意,它无法拦截非 Axios 发出的请求(例如直接调用 fetch)。

  • fetch-mock:

    如果你的项目使用原生 fetch 或基于 fetch 构建的客户端,应选择 fetch-mock。它直接 monkey-patch 全局 fetch,因此能拦截所有通过 fetch 发出的请求,包括第三方库内部的调用。支持复杂的路由匹配规则和动态响应逻辑,适用于现代浏览器环境或兼容 fetch 的 Node.js 环境(需 polyfill)。但若项目完全基于 Axios 且未使用 fetch,引入它会增加不必要的复杂性。

axios-mock-adapter的README

axios-mock-adapter

Axios adapter that allows to easily mock requests

Installation

Using npm:

$ npm install axios-mock-adapter --save-dev

It's also available as a UMD build:

axios-mock-adapter works on Node as well as in a browser, it works with axios v0.17.0 and above.

Example

Mocking a GET request

const axios = require("axios");
const AxiosMockAdapter = require("axios-mock-adapter");

// This sets the mock adapter on the default instance
const mock = new AxiosMockAdapter(axios);

// Mock any GET request to /users
// arguments for reply are (status, data, headers)
mock.onGet("/users").reply(200, {
  users: [{ id: 1, name: "John Smith" }],
});

axios.get("/users").then(function (response) {
  console.log(response.data);
});

Mocking a GET request with specific parameters

const axios = require("axios");
const AxiosMockAdapter = require("axios-mock-adapter");

// This sets the mock adapter on the default instance
const mock = new AxiosMockAdapter(axios);

// Mock GET request to /users when param `searchText` is 'John'
// arguments for reply are (status, data, headers)
mock.onGet("/users", { params: { searchText: "John" } }).reply(200, {
  users: [{ id: 1, name: "John Smith" }],
});

axios
  .get("/users", { params: { searchText: "John" } })
  .then(function (response) {
    console.log(response.data);
  });

When using params, you must match all key/value pairs passed to that option.

To add a delay to responses, specify a delay amount (in milliseconds) when instantiating the adapter

// All requests using this instance will have a 2 seconds delay:
const mock = new AxiosMockAdapter(axiosInstance, { delayResponse: 2000 });

You can restore the original adapter (which will remove the mocking behavior)

mock.restore();

You can also reset the registered mock handlers with resetHandlers

mock.resetHandlers();

You can reset both registered mock handlers and history items with reset

mock.reset();

reset is different from restore in that restore removes the mocking from the axios instance completely, whereas reset only removes all mock handlers that were added with onGet, onPost, etc. but leaves the mocking in place.

Mock a low level network error

// Returns a failed promise with Error('Network Error');
mock.onGet("/users").networkError();

// networkErrorOnce can be used to mock a network error only once
mock.onGet("/users").networkErrorOnce();

Mock a network timeout

// Returns a failed promise with Error with code set to 'ECONNABORTED'
mock.onGet("/users").timeout();

// timeoutOnce can be used to mock a timeout only once
mock.onGet("/users").timeoutOnce();

Passing a function to reply

mock.onGet("/users").reply(function (config) {
  // `config` is the axios config and contains things like the url

  // return an array in the form of [status, data, headers]
  return [
    200,
    {
      users: [{ id: 1, name: "John Smith" }],
    },
  ];
});

Passing a function to reply that returns an axios request, essentially mocking a redirect

mock.onPost("/foo").reply(function (config) {
  return axios.get("/bar");
});

Using a regex

mock.onGet(/\/users\/\d+/).reply(function (config) {
  // the actual id can be grabbed from config.url

  return [200, {}];
});

Using variables in regex

const usersUri = "/users";
const url = new RegExp(`${usersUri}/*`);

mock.onGet(url).reply(200, users);

Specify no path to match by verb alone

// Reject all POST requests with HTTP 500
mock.onPost().reply(500);

Chaining is also supported

mock.onGet("/users").reply(200, users).onGet("/posts").reply(200, posts);

.replyOnce() can be used to let the mock only reply once

mock
  .onGet("/users")
  .replyOnce(200, users) // After the first request to /users, this handler is removed
  .onGet("/users")
  .replyOnce(500); // The second request to /users will have status code 500
// Any following request would return a 404 since there are
// no matching handlers left

Mocking any request to a given url

// mocks GET, POST, ... requests to /foo
mock.onAny("/foo").reply(200);

.onAny can be useful when you want to test for a specific order of requests

// Expected order of requests:
const responses = [
  ["GET", "/foo", 200, { foo: "bar" }],
  ["POST", "/bar", 200],
  ["PUT", "/baz", 200],
];

// Match ALL requests
mock.onAny().reply((config) => {
  const [method, url, ...response] = responses.shift();
  if (config.url === url && config.method.toUpperCase() === method)
    return response;
  // Unexpected request, error out
  return [500, {}];
});

Requests that do not map to a mock handler are rejected with a HTTP 404 response. Since handlers are matched in order, a final onAny() can be used to change the default behaviour

// Mock GET requests to /foo, reject all others with HTTP 500
mock.onGet("/foo").reply(200).onAny().reply(500);

Mocking a request with a specific request body/data

mock.onPut("/product", { id: 4, name: "foo" }).reply(204);

Using an asymmetric matcher, for example Jest matchers

mock
  .onPost(
    "/product",
    { id: 1 },
    {
      headers: expect.objectContaining({
        Authorization: expect.stringMatching(/^Basic /),
      })
    }
  )
  .reply(204);

Using a custom asymmetric matcher (any object that has a asymmetricMatch property)

mock
  .onPost("/product", {
    asymmetricMatch: function (actual) {
      return ["computer", "phone"].includes(actual["type"]);
    },
  })
  .reply(204);

.passThrough() forwards the matched request over network

// Mock POST requests to /api with HTTP 201, but forward
// GET requests to server
mock
  .onPost(/^\/api/)
  .reply(201)
  .onGet(/^\/api/)
  .passThrough();

Recall that the order of handlers is significant

// Mock specific requests, but let unmatched ones through
mock
  .onGet("/foo")
  .reply(200)
  .onPut("/bar", { xyz: "abc" })
  .reply(204)
  .onAny()
  .passThrough();

Note that passThrough requests are not subject to delaying by delayResponse.

If you set onNoMatch option to passthrough all requests would be forwarded over network by default

// Mock all requests to /foo with HTTP 200, but forward
// any others requests to server
const mock = new AxiosMockAdapter(axiosInstance, { onNoMatch: "passthrough" });

mock.onAny("/foo").reply(200);

Using onNoMatch option with throwException to throw an exception when a request is made without match any handler. It's helpful to debug your test mocks.

const mock = new AxiosMockAdapter(axiosInstance, { onNoMatch: "throwException" });

mock.onAny("/foo").reply(200);

axios.get("/unexistent-path");

// Exception message on console:
//
// Could not find mock for: 
// {
//   "method": "get",
//   "url": "http://localhost/unexistent-path"
// }

As of 1.7.0, reply function may return a Promise:

mock.onGet("/product").reply(function (config) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      if (Math.random() > 0.1) {
        resolve([200, { id: 4, name: "foo" }]);
      } else {
        // reject() reason will be passed as-is.
        // Use HTTP error status code to simulate server failure.
        resolve([500, { success: false }]);
      }
    }, 1000);
  });
});

Composing from multiple sources with Promises:

const normalAxios = axios.create();
const mockAxios = axios.create();
const mock = new AxiosMockAdapter(mockAxios);

mock
  .onGet("/orders")
  .reply(() =>
    Promise.all([
      normalAxios.get("/api/v1/orders").then((resp) => resp.data),
      normalAxios.get("/api/v2/orders").then((resp) => resp.data),
      { id: "-1", content: "extra row 1" },
      { id: "-2", content: "extra row 2" },
    ]).then((sources) => [
      200,
      sources.reduce((agg, source) => agg.concat(source)),
    ])
  );

History

The history property allows you to enumerate existing axios request objects. The property is an object of verb keys referencing arrays of request objects.

This is useful for testing.

describe("Feature", () => {
  it("requests an endpoint", (done) => {
    const mock = new AxiosMockAdapter(axios);
    mock.onPost("/endpoint").replyOnce(200);

    feature
      .request()
      .then(() => {
        expect(mock.history.post.length).toBe(1);
        expect(mock.history.post[0].data).toBe(JSON.stringify({ foo: "bar" }));
      })
      .then(done)
      .catch(done.fail);
  });
});

You can clear the history with resetHistory

mock.resetHistory();