axios-mock-adapter、fetch-mock、msw、nock は、JavaScript アプリケーションにおける HTTP リクエストをシミュレートするための主要なライブラリです。これらは、外部 API に依存せずにテストを実行したり、バックエンドが未完成の状態でフロントエンドの開発を進めたりするために不可欠なツールです。
axios-mock-adapter は、人気のある HTTP クライアントである Axios に特化して設計されており、Axios の内部アダプターをフックしてリクエストをインターセプトします。一方、fetch-mock はネイティブの Fetch API をグローバルに置き換えることで動作し、ブラウザ環境での Fetch 利用を前提としています。
msw (Mock Service Worker) は、Service Worker を利用してネットワークレベルでリクエストを横取りする全く異なるアプローチを採用しています。これにより、アプリケーションコードの変更なしに、ブラウザと Node.js の両方で同じモック定義を再利用できます。
最後に nock は、Node.js 環境に特化した老舗のライブラリで、HTTP クライアントを直接モックするのではなく、Node の HTTP モジュールレベルで外部サーバーへの実際の接続をフックします。主にサーバーサイドのテストや統合テストで使用されます。
現代の Web 開発において、外部 API との通信を伴うコードをテストすることは避けて通れない課題です。ネットワークの状態に左右されず、高速かつ安定したテストを実行するためには、HTTP リクエストを適切にモック(偽装)する必要があります。axios-mock-adapter、fetch-mock、msw、nock はいずれもこの目的を果たしますが、そのアプローチと適したシナリオは大きく異なります。
本記事では、これら 4 つのライブラリを技術的な観点から比較し、実際のコード例を通じてそれぞれの特徴と使い分けを解説します。
まず、これらが「どこで」「どのように」リクエストを傍受しているかを理解することが重要です。
axios-mock-adapter は、Axios ライブラリの内部アダプターを置き換えます。Axios が HTTP リクエストを送ろうとした瞬間に横取りし、定義されたレスポンスを返します。そのため、実際のネットワーク通信は一切発生しません。
fetch-mock は、グローバルな fetch 関数そのものを置き換えます。ブラウザや Node.js 環境において、コードが fetch() を呼び出すと、本来の機能ではなくこのライブラリのロジックが実行されます。
msw (Mock Service Worker) は、Service Worker というブラウザの機能(および Node.js でのエミュレーション)を利用します。アプリケーションとネットワークの間に介入するため、コード側は「実際の API を叩いている」と錯覚します。これが最大の強みで、アプリケーションコードを一切変更せずにモックを適用できます。
nock は、Node.js の http / https モジュールレベルでフックします。Axios や Fetch などが内部で Node の HTTP モジュールを使用するため、結果としてすべての outgoing HTTP リクエストを捕捉できます。これは純粋に Node.js 環境限定の技術です。
各ライブラリがどのように定義されるかを見てみましょう。
Axios インスタンスを渡して初期化し、チェーン構文でルールを定義します。
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
const mock = new MockAdapter(axios);
// GET リクエストのモック
mock.onGet('/users').reply(200, [
{ id: 1, name: 'Alice' }
]);
グローバルな fetch をターゲットに設定します。
import fetchMock from 'fetch-mock';
// GET リクエストのモック
fetchMock.get('/users', {
body: [{ id: 1, name: 'Alice' }],
status: 200
});
Service Worker を登録し、ハンドラーを定義します。これが唯一、非同期で初期化が必要なケースです。
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const handlers = [
http.get('/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice' }
]);
})
];
const server = setupServer(...handlers);
// テスト前後で起動・停止
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
ベース URL を指定し、パスとメソッドをチェーンで定義します。
import nock from 'nock';
import axios from 'axios';
// ベース URL を指定
nock('https://api.example.com')
.get('/users')
.reply(200, [
{ id: 1, name: 'Alice' }
]);
// この後、axios.get('https://api.example.com/users') を呼ぶとモックされる
静的なデータだけでなく、リクエストの内容に応じてレスポンスを変えたい場合があります。
コールバック関数を渡すことで、リクエストオブジェクトに基づいて動的にレスポンスを生成できます。
mock.onGet('/users').reply((config) => {
const { token } = config.headers;
if (token === 'invalid') {
return [401, { error: 'Unauthorized' }];
}
return [200, { data: 'success' }];
});
関数を返すことで同様の処理が可能です。
fetchMock.get('/users', (url, opts) => {
const token = opts.headers?.token;
if (token === 'invalid') {
return { status: 401, body: { error: 'Unauthorized' } };
}
return { status: 200, body: { data: 'success' } };
});
ハンドラー関数内で req オブジェクトを自由に操作できます。これが最も柔軟性が高いです。
http.get('/users', ({ request }) => {
const token = request.headers.get('token');
if (token === 'invalid') {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return HttpResponse.json({ data: 'success' });
});
関数を渡して動的なレスポンスを生成できます。
nock('https://api.example.com')
.get('/users')
.reply(200, (uri, requestBody) => {
// requestBody や uri を使って動的にデータを生成
return { data: 'success', path: uri };
});
この選定において最も重要な要素の一つが、どこでテストを実行するかです。
axios-mock-adapter: ブラウザ・Node.js 両方で動作しますが、Axios が必須です。Fetch API には対応していません。fetch-mock: 本来はブラウザ向けですが、Node.js でも global.fetch をポリフィルすれば動作します。ただし、設定の手間がかかります。msw: ブラウザと Node.js で同じコードが動きます。Service Worker をブラウザで使用し、Node.js では内部エミュレーターを使用するため、ユニバーサルなテストスイートに最適です。nock: Node.js 専用です。ブラウザのバンドルに含めようとするとエラーになります。サーバーサイドの統合テストに特化しています。ネットワークエラーやタイムアウトを再現する能力も重要です。
ネットワークエラーをシミュレートするには、Promise を reject させる関数を返します。
mock.onGet('/users').reply(() => {
return new Promise(() => {
// 解決しないことでタイムアウトを模倣するか、reject を呼ぶ
throw new Error('Network Error');
});
});
// または単純に
mock.onGet('/users').networkError();
throws オプションや、reject する Promise を返すことで表現します。
fetchMock.get('/users', 500); // ステータスコード
// または
fetchMock.get('/users', { throws: new TypeError('Failed to fetch') });
networkError ヘルパーが用意されており、非常に直感的です。
import { http, HttpResponse } from 'msw';
http.get('/users', () => {
return HttpResponse.error(); // ネットワークエラーを発生させる
});
replyWithError メソッドが用意されています。
nock('https://api.example.com')
.get('/users')
.replyWithError('Connection refused');
テスト間で状態が漏れないようにすることは、安定したテスト_suite_に不可欠です。
axios-mock-adapter: mock.reset() または mock.restore() を呼び出す必要があります。忘记すると、他のテストに影響します。fetch-mock: fetchMock.restore() でグローバルな状態を元に戻す必要があります。msw: server.resetHandlers() でランタイムのハンドラーをリセットし、server.close() で停止します。この明示的なライフサイクル管理が、信頼性の高いテストを支えています。nock: nock.cleanAll() でインターセプターを削除します。また、nock.isActive() を使って未消費のモックがないか確認する機能もあり、厳密なテストに向いています。それぞれの強みを踏まえ、以下のような基準で選定することをお勧めします。
フロントエンドのユニットテストで Axios を使っている
→ axios-mock-adapter が最も手っ取り早く、設定も最小限で済みます。
Fetch API を使っており、ブラウザ環境のみでテストする
→ fetch-mock が妥当ですが、将来的な拡張性を考えると msw への移行を検討する価値があります。
開発環境全体(ブラウザ・Node)で統一されたモックを使いたい / API サーバーの代わりが欲しい
→ msw が唯一無二の選択肢です。特に、バックエンドが未完成の状態でフロントエンドを開発する際に絶大な威力を発揮します。
Node.js でサーバーサイドロジックや外部 API 連携をテストする
→ nock が最強のツールです。クライアントライブラリに依存せず、実際の HTTP レベルで検証できるため、信頼性が極めて高いです。
| 特徴 | axios-mock-adapter | fetch-mock | msw | nock |
|---|---|---|---|---|
| 対象クライアント | Axios のみ | Fetch API | 任意 (ネットワーク層) | 任意 (Node HTTP) |
| 動作環境 | Browser / Node | Browser / Node* | Browser / Node | Node のみ |
| アプローチ | アダプター置換 | グローバル関数置換 | Service Worker | HTTP モジュールフック |
| コード変更 | 必要 (インスタンス登録) | 必要 (global 置換) | 不要 | 不要 |
| API 定義の共有 | 困難 | 困難 | 容易 | 困難 |
| 学習コスト | 低 | 中 | 中〜高 | 中 |
* Node.js ではポリフィルが必要
「どれが最高か」という問いに対する唯一の正解はありません。しかし、現代のフロントエンドアーキテクチャにおいては、msw が提供する「ネットワークレベルのモック」と「環境非依存性」は、開発体験を大きく向上させる可能性を秘めています。特に、モック定義をテストと開発サーバーの両方で共有できる点は、チーム全体の生産性を高めます。
一方、既存の Axios ベースのプロジェクトで手軽にテストを書きたいなら axios-mock-adapter は依然として有効です。また、サーバーサイドの堅牢なテストには nock が欠かせません。
プロジェクトの技術スタックとテストの目的に合わせて、これらのツールを適切に使い分けることが、質の高いアプリケーション開発への近道となります。
プロジェクトで Axios を既に使用しており、ユニットテスト内でシンプルにリクエストとレスポンスを定義したい場合に最適です。セットアップが非常に簡単で、Axios インスタンスに直接紐付けて使用できるため、小規模なテストスイートや特定のコンポーネントの隔離テストに向いています。ただし、Axios に依存しているため、Fetch API や他のクライアントを使う場合には利用できません。
ネイティブの Fetch API を使用しているプロジェクトで、ブラウザ環境でのみテストを実行する場合に適しています。グローバルな Fetch 関数を置き換えるため、実装詳細に依存しないテストが可能です。しかし、Node.js 環境での実行には追加の設定(global-fetch のポリフィルなど)が必要になることが多く、最近ではより汎用性の高い MSW に移行するケースが増えています。
フロントエンドとバックエンドの両方で同じモック定義を共有したい場合や、アプリケーションコードを変更せずにネットワーク層をモックしたい場合に最適な選択肢です。Service Worker を使用するため、実際のネットワークリクエストと同様の挙動を再現でき、開発中の API サーバーの代わりとしても機能します。学習コストはやや高いですが、長期的な保守性と開発体験の向上が見込めます。
Node.js 環境でのサーバーサイドテストや、外部 API との統合テストを実行する場合に強力なツールです。HTTP クライアントの種類(Axios, Fetch, request など)を問わず、出力される HTTP リクエストをインターセプトできるため、ライブラリに依存しないテストが可能です。ただし、ブラウザ環境では動作しないため、フロントエンドのユニットテストには使用できません。
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();