axios-mock-adapter、fetch-mock、mockttp、nock は、フロントエンドおよび Node.js 開発において外部 API への通信をシミュレートするためのライブラリです。これらはテスト環境でのネットワーク分離、オフライン開発、エッジケースの再現に不可欠ですが、アプローチが異なります。axios-mock-adapter は Axios インスタンスに直接フックし、fetch-mock はグローバルな fetch API を置き換えます。nock は Node.js の HTTP モジュールレベルで動作し、mockttp は実際のプロキシサーバーを立ててトラフィックを傍受・制御します。
現代のフロントエンド開発において、外部 API との連携は避けられません。しかし、テスト実行時に実際のネットワーク通信を行うのは、遅延、不安定性、そしてテスト環境の汚染というリスクをもたらします。これを解決するのが HTTP モックツールです。
今回は、代表的な 4 つのパッケージ(axios-mock-adapter、fetch-mock、mockttp、nock)を比較し、それぞれのアーキテクチャ、適したユースケース、そして実装の違いを明らかにします。
これらのツールは、ネットワーク通信を「どこで」横取りするかという点で大きく異なります。
axios-mock-adapter は、Axios の内部アダプターを置き換えます。ネットワーク層には到達せず、Axios インスタンスが呼ばれた瞬間にインターセプトします。
// axios-mock-adapter: Axios インスタンスに直接結合
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
const mock = new MockAdapter(axios);
mock.onGet('/users').reply(200, { data: ['Alice'] });
// この呼び出しは実際にはネットワークに出ない
const response = await axios.get('/users');
fetch-mock は、グローバルな fetch 関数自体をモック関数に置き換えます。ブラウザや Node.js 上の window.fetch または global.fetch を乗っ取る形です。
// fetch-mock: グローバル fetch を置き換え
import fetchMock from 'fetch-mock';
fetchMock.get('/users', { data: ['Alice'] });
// fetch 関数を使うあらゆるコードがモックされる
const response = await fetch('/users');
nock は、Node.js の http および https モジュールをフックします。アプリケーション層ではなく、ランタイムのネットワーク出力直前でインターセプトするため、使用する HTTP クライアントを選びません。
// nock: Node.js の http モジュールをインターセプト
import nock from 'nock';
import axios from 'axios';
nock('http://api.example.com')
.get('/users')
.reply(200, { data: ['Alice'] });
// axios でも node-fetch でも動作する
const response = await axios.get('http://api.example.com/users');
mockttp は、実際の HTTP プロキシサーバーを起動します。クライアントはこのプロキシ経由で通信し、mockttp がトラフィックを傍受してレスポンスを返します。ブラウザ・Node.js 問わず、プロキシ設定さえできれば動作します。
// mockttp: 実プロキシサーバーを起動
import mockttp from 'mockttp';
const mockServer = await mockttp.getLocal();
await mockServer.start();
await mockServer.forGet('/users').thenReply(200, JSON.stringify({ data: ['Alice'] }));
// プロキシ経由で通信させる必要がある(設定による)
// 実際のネットワークスタックを通るため挙動が現実に近い
どの HTTP クライアントを使っているかで、選択肢は自然と絞られます。
| パッケージ | 主な対象クライアント | 環境 | 特徴 |
|---|---|---|---|
| axios-mock-adapter | Axios のみ | ブラウザ / Node.js | Axios 専用。設定が簡単。 |
| fetch-mock | fetch API | ブラウザ / Node.js (ポリフィル要) | 標準 API 準拠。ライブラリ非依存。 |
| nock | http/https モジュール全体 | Node.js のみ | クライアント非依存。ブラウザ不可。 |
| mockttp | すべて(プロキシ経由) | ブラウザ / Node.js | 環境非依存。設定がやや複雑。 |
axios-mock-adapter を選ぶべきは、Axios を使っている場合です。他方、nock は Node.js 環境で、axios、node-fetch、got など複数のクライアントが混在しているプロジェクトで強力です。
// nock はクライアントを選ばない例
import nock from 'nock';
import got from 'got'; //別のライブラリ
nock('https://api.service.com').post('/login').reply(200, { token: 'xyz' });
// axios でも got でも同じモックが効く
await got.post('https://api.service.com/login');
一方、fetch-mock は fetch を使う場合に有効ですが、Node.js で使う際は node-fetch などのポリフィルが正しくグローバルに設定されているか確認が必要です。
// fetch-mock: ポリフィル環境での注意点
import 'isomorphic-fetch'; // グローバル fetch を保証
import fetchMock from 'fetch-mock';
fetchMock.mock('/data', 200);
// グローバル fetch が書き換わっているか確認が必要
単にデータを返すだけでなく、ネットワークの不具合を再現できるかがテストの質を左右します。
mockttp はこの点で最も柔軟です。実際のサーバーとして動作するため、接続の切断、SSL エラー、極端な遅延などを物理的に再現できます。
// mockttp: 複雑な挙動の再現
await mockServer.forGet('/slow').thenCallback(async () => {
await new Promise(r => setTimeout(r, 5000)); // 5 秒遅延
return { status: 200, json: { slow: true } };
});
await mockServer.forGet('/crash').thenSocketHangUp(); // 接続強制切断
axios-mock-adapter や fetch-mock も遅延やエラーを再現できますが、あくまで Promise の解決・拒否としての再現です。
// axios-mock-adapter: エラーと遅延
mock.onGet('/error').reply(500, { msg: 'Server Error' });
mock.onGet('/timeout').reply(200, { data: 'ok' }, { delay: 2000 });
// fetch-mock: エラーと遅延
fetchMock.get('/error', 500);
fetchMock.get('/timeout', { body: { ok: true }, delay: 2000 });
nock も同様に、エラー状態や遅延をシミュレート可能です。
// nock: エラーと遅延
nock('http://api.test.com')
.get('/fail')
.reply(503, 'Service Unavailable');
nock('http://api.test.com')
.get('/slow')
.delay(2000)
.reply(200, { ok: true });
nock は長年 Node.js エコシステムの標準でしたが、近年メンテナンスの頻度が低下しており、最新の Node.js バージョン(特に v20 以降)や ESM モジュールとの相性で問題が報告されることがあります。新しい Node.js プロジェクトでは、代替手段を検討するか、十分にテストしてから採用することをお勧めします。
fetch-mock も同様に、メンテナンス頻度が落ち着いており、大規模な機能追加は期待できません。しかし、基本的な fetch モックとしては依然として安定しています。
mockttp は比較的新しく、活発に開発が続いています。TypeScript の型定義も充実しており、現代的な開発体験を提供します。
最終的な選択は、あなたのプロジェクトの「環境」と「使用するライブラリ」で決まります。
axios-mock-adapter が最も簡単で確実です。設定の手間が少なく、テストコードが読みやすくなります。fetch-mock が自然な選択です。ただし、Node.js 環境ではポリフィルの設定に注意してください。nock が有力ですが、メンテナンス状況を慎重に評価してください。安定性が最優先なら mockttp の Node.js 利用も検討します。mockttp が唯一の現実的な選択肢です。プロキシ方式であるため、環境に依存せず、最も現実に近い挙動を再現できます。| 特徴 | axios-mock-adapter | fetch-mock | nock | mockttp |
|---|---|---|---|---|
| インターセプト箇所 | Axios インスタンス | グローバル fetch | Node.js http モジュール | 実プロキシサーバー |
| 対応クライアント | Axios のみ | fetch API | すべて (Node.js) | すべて (プロキシ対応) |
| 対応環境 | ブラウザ / Node | ブラウザ / Node | Node.js のみ | ブラウザ / Node |
| セットアップの手間 | 最小 | 小 | 小 | 中 (プロキシ起動) |
| 再現できる挙動 | 基本 + 遅延 | 基本 + 遅延 | 基本 + 遅延 | 高度 (切断、SSL 等) |
| 推奨ケース | Axios プロジェクト | fetch 使用プロジェクト | 従来型 Node.js プロジェクト | クロスプラットフォーム・高度なテスト |
テストの信頼性を高めるためには、実際のネットワーク挙動をどこまで忠実に再現するかが鍵になります。単純なデータ返却で十分な場合は軽量なツールを、ネットワークの不確実性までテストしたい場合は mockttp のような強力なツールを選ぶなど、プロジェクトの成熟度に合わせた選択が重要です。
プロジェクトで Axios を既に採用しており、テストコード内でシンプルにリクエストとレスポンスを定義したい場合に選択します。セットアップが最も簡単で、特定の Axios インスタンスのみをモック化できるため、他の通信に影響を与えたくない単体テストに最適です。ただし、ネイティブの fetch や他の HTTP クライアントには対応していないため、技術スタックが Axios に依存している必要があります。
ブラウザ環境でのテストや、ネイティブ fetch API を直接使用しているプロジェクトに適しています。グローバルな fetch をオーバーライドするため、ライブラリを問わず fetch を使っていれば動作します。ただし、Node.js 環境での利用にはポリフィルが必要になる場合があり、並行して実行されるテスト間の状態管理に注意が必要です。
ブラウザと Node.js の両方で動作する統一されたテスト環境を構築したい場合や、複雑なネットワーク挙動(遅延、切断、SSL エラーなど)を再現したい場合に選択します。実際のプロキシサーバーとして動作するため、実機に近い挙動を検証できます。設定はやや重くなりますが、クロスプラットフォームな E2E テストや、通信内容の詳細な検証が必要な高度なシナリオで真価を発揮します。
Node.js 環境でバックエンドサービスやサーバーサイドレンダリングのテストを行う場合に選択します。HTTP/HTTPS モジュールの低レベルをインターセプトするため、axios、node-fetch、request などあらゆる HTTP クライアントと互換性があります。ただし、ブラウザ環境では動作しないため、フロントエンドのみのテストには不向きです。メンテナンス状況を確認し、最新の Node.js バージョンとの互換性を評価した上で採用してください。
Axios adapter that allows to easily mock requests
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.
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)),
])
);
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();