name: mock-server description: 创建和管理模拟API服务器用于开发和测试。
模拟服务器技能
创建和管理模拟API服务器用于开发和测试。
指令
您是模拟API服务器专家。当被调用时:
-
创建模拟服务器:
- 从OpenAPI规范生成模拟API端点
- 创建自定义模拟响应
- 模拟不同响应场景
- 模拟REST和GraphQL API
- 处理各种HTTP方法
-
配置行为:
- 设置响应延迟/延迟
- 模拟错误条件
- 根据输入返回不同响应
- 实现状态管理
- 模拟认证
-
高级场景:
- 模拟网络故障
- 随机错误注入
- 速率限制模拟
- 条件响应
- CORS配置
-
集成:
- 代理到真实API
- 记录和重放请求
- 生成模拟数据
- 与测试框架集成
使用示例
@mock-server
@mock-server --from-openapi
@mock-server --port 3000
@mock-server --with-delays
@mock-server --graphql
JSON 服务器(简单模拟)
快速设置
# 安装
npm install -g json-server
# 创建 db.json
cat > db.json << EOF
{
"users": [
{ "id": 1, "name": "John Doe", "email": "john@example.com" },
{ "id": 2, "name": "Jane Smith", "email": "jane@example.com" }
],
"posts": [
{ "id": 1, "title": "Hello World", "userId": 1 },
{ "id": 2, "title": "Mock APIs", "userId": 2 }
]
}
EOF
# 启动服务器
json-server --watch db.json --port 3000
可用端点(自动生成)
# GET 所有用户
curl http://localhost:3000/users
# GET 用户通过ID
curl http://localhost:3000/users/1
# POST 新用户
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name": "Bob", "email": "bob@example.com"}'
# PUT 更新用户
curl -X PUT http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"id": 1, "name": "John Updated", "email": "john.new@example.com"}'
# PATCH 部分更新
curl -X PATCH http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"name": "John Patched"}'
# DELETE 用户
curl -X DELETE http://localhost:3000/users/1
# 查询参数
curl "http://localhost:3000/users?_page=1&_limit=10"
curl "http://localhost:3000/users?_sort=name&_order=asc"
curl "http://localhost:3000/posts?userId=1"
自定义路由
// routes.json
{
"/api/*": "/$1",
"/users/:id/posts": "/posts?userId=:id",
"/auth/login": "/login"
}
// 使用自定义路由启动
json-server db.json --routes routes.json
模拟服务工作者 (MSW)
设置
npm install --save-dev msw
REST API 模拟
// src/mocks/handlers.js
import { http, HttpResponse } from 'msw';
export const handlers = [
// GET users
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' }
]);
}),
// GET user by ID
http.get('/api/users/:userId', ({ params }) => {
const { userId } = params;
// 模拟未找到
if (userId === '999') {
return new HttpResponse(null, { status: 404 });
}
return HttpResponse.json({
id: userId,
name: 'John Doe',
email: 'john@example.com'
});
}),
// POST 创建用户
http.post('/api/users', async ({ request }) => {
const data = await request.json();
// 模拟验证错误
if (!data.email) {
return HttpResponse.json(
{ error: 'Email is required' },
{ status: 400 }
);
}
return HttpResponse.json(
{
id: '123',
...data,
createdAt: new Date().toISOString()
},
{ status: 201 }
);
}),
// PUT 更新用户
http.put('/api/users/:userId', async ({ params, request }) => {
const { userId } = params;
const data = await request.json();
return HttpResponse.json({
id: userId,
...data,
updatedAt: new Date().toISOString()
});
}),
// DELETE 用户
http.delete('/api/users/:userId', ({ params }) => {
return new HttpResponse(null, { status: 204 });
}),
// 模拟延迟
http.get('/api/slow-endpoint', async () => {
await delay(2000); // 2秒延迟
return HttpResponse.json({ message: 'Slow response' });
}),
// 模拟随机错误
http.get('/api/unreliable', () => {
if (Math.random() > 0.5) {
return new HttpResponse(null, { status: 500 });
}
return HttpResponse.json({ status: 'ok' });
}),
// 认证
http.post('/api/auth/login', async ({ request }) => {
const { email, password } = await request.json();
if (email === 'user@example.com' && password === 'password') {
return HttpResponse.json({
accessToken: 'mock-jwt-token',
refreshToken: 'mock-refresh-token',
expiresIn: 3600
});
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
})
];
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
浏览器设置
// src/mocks/browser.js
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// src/index.js
if (process.env.NODE_ENV === 'development') {
const { worker } = await import('./mocks/browser');
await worker.start();
}
Node.js 设置(测试)
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/tests/setup.js
import { server } from '../mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Prism (基于OpenAPI)
从OpenAPI规范
# 安装
npm install -g @stoplight/prism-cli
# 从OpenAPI规范启动模拟服务器
prism mock openapi.yaml
# 指定端口
prism mock openapi.yaml --port 4010
# 启用验证
prism mock openapi.yaml --errors
# 基于示例的动态响应
prism mock openapi.yaml --dynamic
OpenAPI示例
openapi: 3.0.0
info:
title: Mock API
version: 1.0.0
paths:
/api/users:
get:
responses:
'200':
description: Success
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
examples:
success:
value:
- id: "1"
name: "John Doe"
email: "john@example.com"
/api/users/{userId}:
get:
parameters:
- name: userId
in: path
required: true
schema:
type: string
responses:
'200':
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/User'
examples:
user:
value:
id: "1"
name: "John Doe"
email: "john@example.com"
'404':
description: Not found
content:
application/json:
examples:
notFound:
value:
error: "User not found"
components:
schemas:
User:
type: object
properties:
id:
type: string
name:
type: string
email:
type: string
Express 模拟服务器
自定义实现
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
// 随机延迟的中间件
app.use((req, res, next) => {
const delay = Math.random() * 1000; // 0-1秒
setTimeout(next, delay);
});
// 认证中间件
app.use('/api/*', (req, res, next) => {
const token = req.headers.authorization;
if (!token || !token.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
});
// 内存数据库
let users = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' }
];
// GET 所有用户
app.get('/api/users', (req, res) => {
const { page = 1, limit = 10 } = req.query;
const start = (page - 1) * limit;
const end = start + parseInt(limit);
const paginatedUsers = users.slice(start, end);
res.json({
data: paginatedUsers,
meta: {
page: parseInt(page),
limit: parseInt(limit),
total: users.length,
totalPages: Math.ceil(users.length / limit)
}
});
});
// GET 用户通过ID
app.get('/api/users/:id', (req, res) => {
const user = users.find(u => u.id === req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// POST 创建用户
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
// 验证
if (!name || !email) {
return res.status(400).json({
error: 'Validation failed',
details: {
name: !name ? 'Name is required' : undefined,
email: !email ? 'Email is required' : undefined
}
});
}
// 检查重复邮箱
if (users.some(u => u.email === email)) {
return res.status(409).json({ error: 'Email already exists' });
}
const newUser = {
id: String(Date.now()),
name,
email,
createdAt: new Date().toISOString()
};
users.push(newUser);
res.status(201).json(newUser);
});
// PUT 更新用户
app.put('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === req.params.id);
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users[index] = {
...users[index],
...req.body,
updatedAt: new Date().toISOString()
};
res.json(users[index]);
});
// PATCH 部分更新
app.patch('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === req.params.id);
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users[index] = {
...users[index],
...req.body,
updatedAt: new Date().toISOString()
};
res.json(users[index]);
});
// DELETE 用户
app.delete('/api/users/:id', (req, res) => {
const index = users.findIndex(u => u.id === req.params.id);
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(index, 1);
res.status(204).send();
});
// 错误模拟端点
app.get('/api/simulate-error', (req, res) => {
const errorType = req.query.type || 'random';
if (errorType === 'timeout') {
// 从不响应(超时)
return;
}
if (errorType === 'random' && Math.random() > 0.5) {
return res.status(500).json({ error: 'Internal server error' });
}
res.json({ status: 'ok' });
});
// 健康检查
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Mock server running on http://localhost:${PORT}`);
});
GraphQL 模拟服务器
使用 Apollo Server
const { ApolloServer, gql } = require('apollo-server');
const { MockList } = require('@graphql-tools/mock');
// 模式
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts: [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
`;
// 模拟
const mocks = {
User: () => ({
id: () => Math.random().toString(36).substring(7),
name: () => 'John Doe',
email: () => 'john@example.com',
posts: () => new MockList([2, 6])
}),
Post: () => ({
id: () => Math.random().toString(36).substring(7),
title: () => 'Sample Post',
content: () => 'This is mock content'
}),
Query: () => ({
users: () => new MockList([10, 20]),
posts: () => new MockList([20, 40])
})
};
const server = new ApolloServer({
typeDefs,
mocks,
mockEntireSchema: false
});
server.listen().then(({ url }) => {
console.log(`GraphQL mock server ready at ${url}`);
});
自定义解析器
const resolvers = {
Query: {
users: () => [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' }
],
user: (_, { id }) => {
const users = [
{ id: '1', name: 'John Doe', email: 'john@example.com' },
{ id: '2', name: 'Jane Smith', email: 'jane@example.com' }
];
return users.find(u => u.id === id);
}
},
Mutation: {
createUser: (_, { name, email }) => ({
id: Math.random().toString(36).substring(7),
name,
email
})
}
};
const server = new ApolloServer({
typeDefs,
resolvers
});
WireMock (基于Java)
Docker 设置
# 在Docker中运行WireMock
docker run -d \
--name wiremock \
-p 8080:8080 \
-v $(pwd)/wiremock:/home/wiremock \
wiremock/wiremock:latest
# 创建映射
mkdir -p wiremock/mappings
cat > wiremock/mappings/users.json << EOF
{
"request": {
"method": "GET",
"url": "/api/users"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": [
{ "id": "1", "name": "John Doe", "email": "john@example.com" }
]
}
}
EOF
模拟数据生成
使用 Faker.js
const { faker } = require('@faker-js/faker');
// 生成模拟用户
function generateUser() {
return {
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
avatar: faker.image.avatar(),
address: {
street: faker.location.streetAddress(),
city: faker.location.city(),
country: faker.location.country()
},
createdAt: faker.date.past().toISOString()
};
}
// 生成多个用户
function generateUsers(count = 10) {
return Array.from({ length: count }, generateUser);
}
// 生成帖子
function generatePost() {
return {
id: faker.string.uuid(),
title: faker.lorem.sentence(),
content: faker.lorem.paragraphs(3),
author: generateUser(),
createdAt: faker.date.past().toISOString()
};
}
高级场景
条件响应
// 根据头部返回不同响应
http.get('/api/users', ({ request }) => {
const acceptLanguage = request.headers.get('Accept-Language');
if (acceptLanguage?.includes('es')) {
return HttpResponse.json({
mensaje: 'Hola Mundo'
});
}
return HttpResponse.json({
message: 'Hello World'
});
});
// 根据查询参数
http.get('/api/users', ({ request }) => {
const url = new URL(request.url);
const format = url.searchParams.get('format');
if (format === 'xml') {
return new HttpResponse(
'<users><user>John</user></users>',
{ headers: { 'Content-Type': 'application/xml' } }
);
}
return HttpResponse.json([{ name: 'John' }]);
});
有状态模拟
let requestCount = 0;
http.get('/api/rate-limit', () => {
requestCount++;
if (requestCount > 10) {
return HttpResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
);
}
return HttpResponse.json({ count: requestCount });
});
网络错误模拟
http.get('/api/network-error', () => {
return HttpResponse.error();
});
// 超时模拟
http.get('/api/timeout', async () => {
await delay(30000); // 30秒超时
return HttpResponse.json({});
});
最佳实践
模拟服务器设计
- 镜像生产API结构
- 使用真实数据
- 实施适当的错误响应
- 支持分页
- 包括速率限制模拟
数据管理
- 使用数据生成器生成真实模拟数据
- 跨请求保持数据一致
- 测试运行之间重置状态
- 支持CRUD操作
- 实施适当的关系
响应场景
- 成功响应 (200, 201, 204)
- 客户端错误 (400, 401, 403, 404, 409)
- 服务器错误 (500, 502, 503)
- 网络错误
- 超时
开发工作流程
- 前端开发前启动模拟服务器
- 为API URL使用环境变量
- 轻松在模拟和真实API之间切换
- 记录可用端点
- 保持模拟数据与API同步
测试
- 使用模拟进行隔离单元测试
- 测试错误处理
- 验证请求验证
- 测试认证流程
- 模拟边缘情况
工具比较
| 工具 | 最佳用途 | 复杂度 | 特性 |
|---|---|---|---|
| JSON Server | 快速原型 | 低 | 自动-CRUD,简单设置 |
| MSW | 浏览器/Node测试 | 中 | 请求拦截,强大 |
| Prism | OpenAPI规范 | 低 | 基于规范,验证 |
| WireMock | 企业,Java | 高 | 记录,匹配 |
| 自定义 Express | 完全控制 | 中 | 完全自定义 |
笔记
- 前端开发期间使用模拟服务器
- 保持模拟响应与API合同同步
- 测试成功和失败场景
- 为更好测试实施真实延迟
- 使用环境变量在模拟/真实API之间切换
- 记录模拟端点和行为
- 测试之间重置模拟状态
- 使用数据生成器生成真实数据
- 为浏览器测试实施适当CORS
- 与API版本一起版本化模拟数据