这些库旨在帮助开发者在前端开发过程中模拟后端 API 响应,或生成逼真的测试数据。msw、miragejs、axios-mock-adapter 和 nock 专注于拦截网络请求或模拟服务器行为,而 faker 专注于生成随机数据,json-server 则提供一个完整的本地 REST 服务器。它们共同解决了前后端并行开发、单元测试隔离以及数据隐私保护等核心工程问题。
在现代前端工程化中,依赖真实后端接口进行开发往往效率低下且不稳定。我们需要可靠的工具来模拟 API 响应、生成测试数据,以便前后端并行开发或编写隔离的单元测试。msw、miragejs、axios-mock-adapter、nock、json-server 和 faker 是生态中最常见的解决方案,但它们的实现原理和适用场景截然不同。本文将从拦截机制、环境支持、数据生成能力等维度进行深度剖析。
选择模拟工具时,最关键的区别在于它是在哪个层面拦截请求。是在浏览器网络层、HTTP 客户端层,还是 Node.js 底层。
msw 使用 Service Worker 在浏览器网络层拦截请求。
// msw: 定义请求处理器
import { http, HttpResponse } from 'msw'
export const handlers = [
http.get('/users', () => {
return HttpResponse.json({ id: 1, name: 'Alice' })
})
]
axios-mock-adapter 直接挂钩到 axios 实例内部。
// axios-mock-adapter: 绑定到实例
import AxiosMockAdapter from 'axios-mock-adapter'
import axios from 'axios'
const mock = new AxiosMockAdapter(axios)
mock.onGet('/users').reply(200, { id: 1, name: 'Alice' })
nock 拦截 Node.js 原生的 HTTP/HTTPS 模块。
// nock: 拦截 Node HTTP 请求
import nock from 'nock'
nock('http://api.example.com')
.get('/users')
.reply(200, { id: 1, name: 'Alice' })
miragejs 在客户端内部模拟一个服务器。
// miragejs: 创建客户端服务器
import { createServer } from 'miragejs'
createServer({
routes() {
this.get('/users', () => {
return { id: 1, name: 'Alice' }
})
}
})
json-server 运行一个独立的 Node.js 进程作为服务器。
# json-server: 命令行启动服务
npx json-server --watch db.json --port 3000
模拟接口时,数据是写死的还是动态生成的,决定了测试的覆盖率。
faker 专门用于生成逼真的随机数据。
faker 包已弃用,新项目请使用 @faker-js/faker。// faker: 生成随机数据
import { faker } from '@faker-js/faker' // 注意包名变化
const user = {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email()
}
msw 支持在处理器中动态返回数据。
faker 在每次请求时生成不同数据。// msw: 动态返回数据
http.get('/users', () => {
return HttpResponse.json({
name: faker.person.fullName()
})
})
miragejs 内置了工厂系统(Factories)来生成数据。
// miragejs: 工厂生成数据
server.loadFactories({
user: Factory.define('user', {
name: faker.person.fullName()
})
})
json-server 依赖静态 JSON 文件或简单的随机生成器。
// json-server: db.json 静态数据
{
"users": [
{ "id": 1, "name": "Alice" }
]
}
axios-mock-adapter 和 nock 通常返回预设的响应。
// nock: 预设响应
nock('http://api.com').get('/users').reply(200, { fixed: true })
运行环境是选择工具时的硬性约束。
| package | 浏览器 | Node.js | 备注 |
|---|---|---|---|
msw | ✅ | ✅ | 浏览器用 SW,Node 用补丁 |
axios-mock-adapter | ✅ | ✅ | 依赖 axios 运行环境 |
miragejs | ✅ | ❌ | 专为浏览器设计 |
json-server | ❌ | ✅ | 作为独立进程运行 |
nock | ❌ | ✅ | 仅限 Node 环境 |
faker | ✅ | ✅ | 纯工具库,无环境限制 |
msw 是少数能无缝切换环境的工具。
// msw: 通用设置
import { setupServer } from 'msw/node' // Node
import { setupWorker } from 'msw/browser' // Browser
nock 和 json-server 仅限于后端或测试脚本。
// nock: 仅用于 Node 测试文件
describe('API Test', () => {
it('fetches data', async () => {
nock('...').get('/').reply(200)
// 运行测试
})
})
miragejs 专注于浏览器开发体验。
// miragejs: 通常在 main.js 中引入
if (process.env.NODE_ENV === 'development') {
require('./mocks')
}
工具对现有代码的侵入程度直接影响迁移成本。
msw 侵入性最低。
// msw: 注册 worker
import { worker } from './mocks/browser'
worker.start()
axios-mock-adapter 侵入性中等。
// axios-mock-adapter: 需要实例引用
import axios from './utils/axiosInstance'
const mock = new AxiosMockAdapter(axios)
json-server 零侵入性。
# 修改环境变量即可切换
API_BASE_URL=http://localhost:3000
miragejs 侵入性较高。
// miragejs: 需要初始化逻辑
createServer({
models: { user: Model },
routes: { ... }
})
在选择长期依赖的库时,维护状态至关重要。
faker: 原包已弃用。存在安全漏洞风险且不再更新。必须 迁移至 @faker-js/faker。miragejs: 目前处于维护模式。核心功能稳定,但新特性更新缓慢。大型项目需评估长期风险。msw: 活跃维护中。社区增长迅速,是目前前端模拟的事实标准。json-server: 稳定维护。适合原型开发,但功能迭代较慢。nock: 稳定维护。Node 测试领域的经典工具。axios-mock-adapter: 稳定维护。作为专用适配器,功能范围固定。msw 是现代前端项目的首选。
json-server 适合快速原型或前端独立开发阶段。
nock 是 Node.js 后端测试的标配。
faker (及其替代者) 是数据生成的必备工具。
@faker-js/faker 来生成动态数据,避免硬编码。axios-mock-adapter 适合老旧项目或纯 Axios 场景。
msw 成本过高,且项目只使用 Axios,它可以继续发挥作用。miragejs 适合需要复杂客户端数据关系的场景。
总结:对于新架构,推荐 msw + @faker-js/faker 组合。对于后端测试,推荐 nock。对于快速原型,推荐 json-server。避免在新项目中使用已弃用的 faker 包。
如果你的项目完全依赖 axios 进行 HTTP 请求,且只需要在单元测试中快速桩化(stub)特定接口,这是一个轻量级的选择。它直接挂钩到 axios 实例,配置简单,但不适用于其他 HTTP 客户端或网络层测试。
适用于需要生成大量逼真假数据(如姓名、地址、信用卡号)的场景。但请注意,原 faker 包已停止维护,新项目应直接使用社区维护的 @faker-js/faker 以避免安全风险和缺失的功能。
适合需要快速搭建一个真实的 REST API 服务器原型的场景,尤其是前端独立开发时。它通过读取 JSON 文件自动提供 CRUD 接口,但不适合复杂的业务逻辑模拟或高性能测试。
适合需要在浏览器内部模拟完整后端逻辑的应用,支持关系建模和延迟模拟。但该项目目前处于维护模式,更新频率较低,新大型项目需评估长期支持风险。
现代前端开发的首选方案,通过 Service Worker 在网络层拦截请求,支持浏览器和 Node.js 环境。它不侵入应用代码,能最真实地模拟网络行为,适合集成测试和开发环境。
专为 Node.js 环境设计,用于拦截底层 HTTP 请求。如果你主要在编写后端服务测试或 Node.js 脚本测试,且需要验证具体的 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();