axios-mock-adapter vs fetch-mock vs mockttp vs nock
前端网络请求 Mock 方案深度对比与选型指南
axios-mock-adapterfetch-mockmockttpnock类似的npm包:

前端网络请求 Mock 方案深度对比与选型指南

axios-mock-adapter、fetch-mock、mockttp 和 nock 都是用于在 JavaScript 测试环境中模拟 HTTP 请求的工具,但它们的工作层级和适用场景截然不同。

axios-mock-adapter 专门针对 Axios 库设计,通过拦截 Axios 内部的适配器层来返回模拟数据,完全不需要真实网络交互,速度极快但仅限于 Axios 项目。

fetch-mock 专注于全局 fetch API 的桩测试(stubbing),它替换了全局或局部的 fetch 函数,非常适合基于原生 fetch 或 polyfill 的前端项目,支持链式调用和灵活的匹配规则。

nock 是 Node.js 生态中的老牌工具,它在底层 HTTP 客户端级别拦截请求(如 http/https 模块),常用于后端集成测试或 Node.js 环境下的端到端验证,但在浏览器环境中无法直接使用。

mockttp 则是一个更现代的跨平台方案,它通过启动一个真实的本地代理服务器来拦截流量,能够同时覆盖浏览器中的 fetch/XHR 和 Node.js 中的各种 HTTP 客户端,支持复杂的流量重放和 SSL 模拟,适合全栈或高保真测试场景。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
axios-mock-adapter03,54167.9 kB972 年前MIT
fetch-mock01,307157 kB81 年前MIT
mockttp08901.99 MB423 天前Apache-2.0
nock013,133181 kB1105 天前MIT

前端网络 Mock 方案深度对决:axios-mock-adapter, fetch-mock, mockttp, nock

在构建高质量的前端应用时,隔离外部依赖是测试策略的核心。我们需要在不依赖真实后端的情况下,验证组件逻辑、错误处理和加载状态。虽然目标一致,但 axios-mock-adapter、fetch-mock、mockttp 和 nock 采用了完全不同的技术路径来实现这一目标。理解它们的底层机制,是做出正确架构决策的关键。

🏗️ 核心架构:拦截层级决定适用范围

这四个工具的根本区别在于它们“切入”网络请求的位置不同。这直接决定了它们能拦截什么,以及在哪里运行。

axios-mock-adapter 工作在应用层。它不触碰网络,而是直接替换 Axios 内部的适配器(Adapter)。当 Axios 准备发送请求时,适配器被_mock_版本接管,直接返回 Promise 结果。

// axios-mock-adapter: 拦截 Axios 内部调用
import Axios from 'axios';
import MockAdapter from 'axios-mock-adapter';

const axios = Axios.create();
const mock = new MockAdapter(axios);

// 直接定义模拟行为,无网络开销
mock.onGet('/users').reply(200, { data: ['Alice'] });

// 触发请求,立即得到模拟结果
const response = await axios.get('/users');

fetch-mock 工作在全局 API 层。它通过替换全局对象中的 fetch 函数(或局部作用域内的引用)来拦截请求。任何调用标准 fetch 的代码都会被它捕获。

// fetch-mock: 替换全局 fetch 函数
import fetchMock from 'fetch-mock';

// 定义匹配规则和响应
fetchMock.get('/users', { body: { data: ['Alice'] }, status: 200 });

// 任何使用 fetch 的地方都会被拦截
const response = await fetch('/users');
const json = await response.json();

nock 工作在 Node.js 底层 I/O 层。它通过 Monkey-patch(修改)Node.js 原生的 http 和 https 模块来拦截出站请求。这意味着它只能运行在 Node 环境中,无法在浏览器中使用。

// nock: 拦截 Node 原生 http 模块 (仅限 Node 环境)
import nock from 'nock';
import http from 'http';

// 定义拦截器
nock('http://api.example.com')
  .get('/users')
  .reply(200, { data: ['Alice'] });

// Node 原生请求或基于 http 模块的库会被拦截
http.get('http://api.example.com/users', (res) => {
  // 处理模拟响应
});

mockttp 工作在传输层(代理模式)。它启动一个真实的本地 HTTP 代理服务器。你将客户端配置为通过该代理发送请求,所有流量都会经过它,从而可以被拦截、修改或记录。这种方式与语言无关,只要支持 HTTP 代理即可。

// mockttp: 启动真实代理服务器
import mockttp from 'mockttp';

const mockServer = mockttp.getLocal();
await mockServer.start();
const port = mockServer.port;

// 定义规则
await mockServer.forGet('/users').thenJson(200, { data: ['Alice'] });

// 客户端需配置代理指向 mockServer
const response = await fetch('http://api.example.com/users', {
  agent: new HttpProxyAgent(`http://localhost:${port}`) // Node 示例
});

⚡ 性能与开销:内存桩 vs 真实网络

性能表现是选择工具时的重要考量,尤其是在运行成千上万个单元测试时。

axios-mock-adapter 和 fetch-mock 属于“内存桩”(In-memory Stubbing)。它们不产生任何网络数据包,不建立 TCP 连接,也不涉及序列化和反序列化开销。请求和响应直接在 JavaScript 引擎内通过 Promise 解析完成。这使得它们的执行速度极快,几乎是瞬时的。

// axios-mock-adapter: 纯内存操作,速度最快
mock.onPost('/login').reply(200, { token: 'xyz' });
await axios.post('/login'); // 微任务级别完成
// fetch-mock: 纯内存操作,速度极快
fetchMock.post('/login', { token: 'xyz' });
await fetch('/login'); // 微任务级别完成

nock 虽然也拦截了底层调用,避免了真实的网络往返,但它仍然模拟了 Node.js Stream 和 HTTP 解析器的行为。相比纯内存桩,它有轻微的开销,但在 Node 环境中依然非常高效。

// nock: 模拟 Node Stream 行为
nock('http://api.com').post('/login').reply(200, { token: 'xyz' });
await request.post('http://api.com/login'); // 接近真实 I/O 流程但无网络延迟

mockttp 是唯一涉及真实网络交互的工具。请求必须经过 TCP 连接到本地代理服务器,进行 HTTP 解析,然后由代理服务器返回响应。这会带来明显的延迟(通常在几毫秒到几十毫秒之间),并且消耗更多的系统资源(端口、线程)。因此,它更适合集成测试或端到端测试,而不适合海量的单元测试。

// mockttp: 真实网络往返,有延迟
await mockServer.forPost('/login').thenJson(200, { token: 'xyz' });
await fetch('http://api.com/login', { agent }); // 经历完整 TCP/HTTP 握手

🌐 环境兼容性:浏览器、Node 与全栈

不同的项目架构对运行环境有不同的要求。

axios-mock-adapter 可以在任何能运行 Axios 的地方工作,包括浏览器、Node.js、React Native 等。只要你的代码引入了 Axios,它就能生效。

// 适用于所有 Axios 运行环境
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
// 浏览器、Node、RN 均可

fetch-mock 主要针对浏览器环境设计,但也支持 Node.js(需配合 node-fetch 或全局 polyfill)。它的核心优势在于能完美模拟浏览器的 fetch 行为,包括 CORS 预检逻辑的模拟(虽然只是模拟,并非真实处理)。

// 主要用于浏览器或带有 fetch polyfill 的 Node 环境
import fetchMock from 'fetch-mock';
// 在 Jest 等环境中通常需配置 global.fetch

nock 严格限定于 Node.js 环境。如果你试图在浏览器打包代码中包含 nock,构建工具会报错,或者运行时会因为找不到 http 模块而崩溃。它是后端测试的专属工具。

// 仅限 Node.js
import nock from 'nock';
// 浏览器中不可用

mockttp 具有最强的跨平台能力。由于它基于代理协议,它可以拦截来自浏览器的请求(通过配置浏览器代理或 Puppeteer/Playwright),也可以拦截 Node.js、Python、Go 等任何语言的请求。这使得它成为全栈测试和微服务集成测试的理想选择。

// 全平台支持
// 1. 浏览器 (通过 Puppeteer 配置代理)
// 2. Node.js (通过 HttpProxyAgent)
// 3. 其他语言 (配置系统代理)
await mockServer.forGet('/data').thenJson(200, { id: 1 });

🛠️ 功能特性:简单模拟 vs 流量工程

在功能深度上,这些工具分为两个梯队。

axios-mock-adapter 和 fetch-mock 专注于“请求 - 响应”的简单映射。它们擅长根据 URL、Method、Body 返回预设的数据。它们支持延迟模拟、错误抛出和基本的重复计数,但在处理复杂场景(如动态修改请求头、SSL 证书验证、WebSocket 升级)时能力有限。

// axios-mock-adapter: 基础模拟
mock.onGet('/users').replyOnce(500); // 仅一次失败
mock.onGet('/users').reply(function(config) {
  // 简单的动态响应
  return [200, { time: Date.now() }];
});
// fetch-mock: 链式模拟
fetchMock
  .mock('/users', 200)
  .mock('/admin', 403);

nock 提供了更底层的控制,特别是在 Node.js 环境中。它可以拦截请求头、验证请求体的具体结构、模拟网络错误(如 ECONNREFUSED),甚至支持请求录制和回放(Record & Playback),这对于回归测试非常有价值。

// nock: 高级验证与录制
nock('http://api.com')
  .post('/users', { name: 'Alice' }) // 严格匹配 Body
  .reply(201);

// 录制模式
nock.recorder.rec();

mockttp 是功能最强大的“流量工程”工具。它不仅支持基本的 Mock,还能:

  1. SSL/TLS 模拟:自动处理 HTTPS 流量,无需配置证书。
  2. 流量修改:在请求发送前或响应返回后动态修改 Header 或 Body。
  3. 规则优先级:支持复杂的规则匹配优先级。
  4. WebSocket 支持:可以模拟 WebSocket 握手和消息。
  5. 调试与观察:提供可视化的请求日志和检查工具。
// mockttp: 流量修改与 SSL
await mockServer.forPost('/upload')
  .withBodyContaining('secret')
  .thenCallback((req) => {
    // 动态修改响应
    return { json: { status: 'received', size: req.body.length } };
  });

// 自动处理 HTTPS
await mockServer.forGet('https://secure.api.com/data').thenJson(200, {});

📊 选型决策矩阵

为了帮助你在实际工程中做出选择,以下是关键维度的对比总结:

特性维度axios-mock-adapterfetch-mocknockmockttp
拦截原理Axios 适配器替换全局 fetch 替换Node http 模块 Patch本地 HTTP 代理服务器
运行环境全平台 (需 Axios)浏览器 / Node (需 fetch)仅 Node.js全平台 (需配置代理)
执行速度⚡️ 极快 (内存级)⚡️ 极快 (内存级)🚀 快 (模拟 I/O)🐢 中等 (真实网络)
真实网络❌ 无❌ 无❌ 无✅ 有 (本地回环)
HTTPS 支持✅ (模拟)✅ (模拟)✅ (模拟)✅ (真实解密/重加密)
WebSocket❌ 不支持❌ 不支持❌ 不支持✅ 支持
主要用途Axios 项目单元测试Fetch 项目单元测试Node 后端集成测试全栈/E2E/复杂集成测试

💡 专家建议与避坑指南

  1. 单元测试首选轻量级方案:如果你的测试用例是纯粹的单元测试(Unit Test),目的是验证组件逻辑而非网络协议,请优先选择 axios-mock-adapter 或 fetch-mock。它们的速度优势能显著缩短 CI/CD 流水线的反馈时间。不要为了“功能强大”而引入 mockttp,那会拖慢你的测试套件。

  2. 警惕 nock 的浏览器误用:很多开发者习惯在后端使用 nock,从而在前端项目中也尝试引入。请务必记住,nock 依赖 Node 核心模块,在浏览器中是完全不可用的。如果你的项目是同构(Isomorphic)的,需要在两端运行测试,请避免使用 nock。

  3. 复杂场景拥抱 mockttp:当你需要测试真实的 SSL 握手、验证 HTTP 代理行为、调试 WebSocket 连接,或者需要在 Playwright/Selenium 等浏览器自动化工具中拦截流量时,mockttp 是唯一的选择。它的配置虽然稍显复杂,但能提供其他工具无法企及的保真度。

  4. 避免混合使用:在一个测试文件中,尽量不要同时使用 fetch-mock 和 mockttp 来拦截同一个请求,这会导致不可预测的行为(例如请求被拦截两次或发生死循环)。明确测试的边界,一个测试用例只使用一种拦截策略。

  5. 清理副作用:无论使用哪个库,务必在测试结束后清理模拟规则。axios-mock-adapter 需要 mock.reset(),fetch-mock 需要 fetchMock.restore(),nock 需要 nock.cleanAll(),mockttp 需要 server.stop()。未清理的模拟规则会污染全局环境,导致后续测试出现诡异的失败。

// 示例:正确的清理模式 (Jest)
afterEach(() => {
  mock.reset(); // axios-mock-adapter
  fetchMock.restore(); // fetch-mock
  nock.cleanAll(); // nock
  // mockServer.stop(); // mockttp (通常在 afterAll 中停止服务器)
});

🏁 总结

没有“最好”的 Mock 工具,只有最适合当前场景的工具。

  • 如果你的技术栈是 Axios 且追求 单元测试速度,axios-mock-adapter 是不二之选。
  • 如果你拥抱 Web 标准 (fetch) 且主要在 浏览器 环境测试,fetch-mock 最为顺手。
  • 如果你在 Node.js 后端 进行集成测试,nock 依然是行业标杆。
  • 如果你面临 全栈挑战、需要 高保真模拟 或 调试复杂网络行为,mockttp 将为你提供最强的火力支持。

理解这些工具背后的设计哲学,能让你的测试策略更加稳健,架构决策更加从容。

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

  • axios-mock-adapter:

    如果你的项目完全依赖 Axios 进行网络请求,且追求极致的单元测试运行速度,这是最佳选择。它配置简单,无需处理全局污染,能够精确控制 Axios 特有的拦截器和配置项。但请注意,它无法模拟非 Axios 发起的请求,也不适用于测试 fetch API 或其他 HTTP 客户端。

  • fetch-mock:

    当你的项目主要使用原生 fetch API 或你希望测试代码与具体的 HTTP 库解耦时,应选择此包。它非常适合前端组件测试,能够轻松模拟各种响应状态、延迟和错误。如果你的构建环境需要严格的全局隔离,它提供了良好的作用域管理功能,但仅局限于 fetch 标准。

  • mockttp:

    如果你需要在全栈环境、浏览器真机测试或复杂集成场景中工作,mockttp 是最强大的工具。它通过真实代理服务器工作,能同时拦截 fetch、axios、XHR 甚至 WebSocket 流量,支持 SSL 加密流量分析和流量重放。虽然启动开销稍大,但它提供的保真度和跨平台能力是其他桩测试工具无法比拟的。

  • nock:

    仅在纯 Node.js 环境下进行后端服务测试或集成测试时选择 nock。它在拦截 Node 原生 http/https 模块方面表现卓越,适合验证微服务间的调用。切勿在浏览器端测试中使用它,因为它依赖 Node 的核心模块,在现代前端架构中已逐渐被更通用的代理方案取代。

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();