mcpField
A
api-mock-server

api-mock-server

github.com/patricio0312rev测试与质量63287 次安装41 周前更新
代码仓库

关于 api-mock-server

面向开发与测试流程的 API 模拟技能,提供使用 MSW、json-server 或自定义方案构建模拟接口的方法。涵盖测试数据、成功与错误场景,以及浏览器和测试环境集成。

主要功能

  1. 01配置 MSW 和 json-server
  2. 02定义接口处理器与夹具
  3. 03构建成功及错误场景
  4. 04集成浏览器、单元和端到端测试
  5. 05支持 GraphQL 模拟

适用场景

  1. 01在开发中模拟后端接口
  2. 02为测试准备固定或生成数据
  3. 03验证错误、空数据和加载状态
  4. 04在端到端测试中拦截 API 请求

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patricio0312rev/skills --skill api-mock-server

安装后在 Claude Code 中输入/api-mock-server确认已加载。

详细介绍

API Mock Server

创建逼真的模拟 API,用于测试和开发。

核心工作流

  1. 选择方案:MSW、json-server、自定义
  2. 定义处理器:模拟端点
  3. 设置测试数据:测试数据
  4. 配置场景:成功/错误状态
  5. 集成测试:在测试套件中使用
  6. 记录模拟配置:API 契约

MSW(Mock Service Worker)

安装
npm install -D msw
npx msw init ./public --save
处理器定义
// mocks/handlers.ts
import { http, HttpResponse, delay } from 'msw';

// Types
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface CreateUserInput {
  name: string;
  email: string;
}

// Fixtures
const users: User[] = [
  { id: '1', name: 'John Doe', email: '[email protected]', role: 'admin' },
  { id: '2', name: 'Jane Smith', email: '[email protected]', role: 'user' },
];

// Handlers
export const handlers = [
  // GET /api/users
  http.get('/api/users', async () => {
    await delay(100);
    return HttpResponse.json(users);
  }),

  // GET /api/users/:id
  http.get('/api/users/:id', async ({ params }) => {
    await delay(50);
    const user = users.find((u) => u.id === params.id);

    if (!user) {
      return new HttpResponse(null, { status: 404 });
    }

    return HttpResponse.json(user);
  }),

  // POST /api/users
  http.post('/api/users', async ({ request }) => {
    await delay(100);
    const body = (await request.json()) as CreateUserInput;

    const newUser: User = {
      id: String(users.length + 1),
      name: body.name,
      email: body.email,
      role: 'user',
    };

    users.push(newUser);

    return HttpResponse.json(newUser, { status: 201 });
  }),

  // PUT /api/users/:id
  http.put('/api/users/:id', async ({ params, request }) => {
    await delay(50);
    const body = (await request.json()) as Partial<User>;
    const index = users.findIndex((u) => u.id === params.id);

    if (index === -1) {
      return new HttpResponse(null, { status: 404 });
    }

    users[index] = { ...users[index], ...body };

    return HttpResponse.json(users[index]);
  }),

  // DELETE /api/users/:id
  http.delete('/api/users/:id', async ({ params }) => {
    await delay(50);
    const index = users.findIndex((u) => u.id === params.id);

    if (index === -1) {
      return new HttpResponse(null, { status: 404 });
    }

    users.splice(index, 1);

    return new HttpResponse(null, { status: 204 });
  }),
];
浏览器端设置
// mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';

export const worker = setupWorker(...handlers);
// main.tsx or app entry
async function enableMocking() {
  if (process.env.NODE_ENV === 'development') {
    const { worker } = await import('./mocks/browser');
    return worker.start({
      onUnhandledRequest: 'bypass',
    });
  }
}

enableMocking().then(() => {
  // Render app
});
Node 端设置(测试)
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
// tests/setup.ts (Jest or Vitest)
import { beforeAll, afterEach, afterAll } from 'vitest';
import { server } from '../mocks/server';

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
测试专用处理器
// tests/users.test.ts
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
import { render, screen, waitFor } from '@testing-library/react';
import { UserList } from '../components/UserList';

describe('UserList', () => {
  it('displays users', async () => {
    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });
  });

  it('handles empty state', async () => {
    server.use(
      http.get('/api/users', () => {
        return HttpResponse.json([]);
      })
    );

    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText('No users found')).toBeInTheDocument();
    });
  });

  it('handles server error', async () => {
    server.use(
      http.get('/api/users', () => {
        return new HttpResponse(null, { status: 500 });
      })
    );

    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText('Error loading users')).toBeInTheDocument();
    });
  });

  it('handles network error', async () => {
    server.use(
      http.get('/api/users', () => {
        return HttpResponse.error();
      })
    );

    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText('Network error')).toBeInTheDocument();
    });
  });
});
GraphQL 模拟
// mocks/graphql-handlers.ts
import { graphql, HttpResponse } from 'msw';

export const graphqlHandlers = [
  graphql.query('GetUsers', () => {
    return HttpResponse.json({
      data: {
        users: [
          { id: '1', name: 'John', email: '[email protected]' },
          { id: '2', name: 'Jane', email: '[email protected]' },
        ],
      },
    });
  }),

  graphql.mutation('CreateUser', ({ variables }) => {
    return HttpResponse.json({
      data: {
        createUser: {
          id: '3',
          name: variables.input.name,
          email: variables.input.email,
        },
      },
    });
  }),

  graphql.query('GetUser', ({ variables }) => {
    if (variables.id === 'not-found') {
      return HttpResponse.json({
        data: { user: null },
        errors: [{ message: 'User not found' }],
      });
    }

    return HttpResponse.json({
      data: {
        user: { id: variables.id, name: 'John', email: '[email protected]' },
      },
    });
  }),
];

Json-Server

配置
// db.json
{
  "users": [
    { "id": "1", "name": "John Doe", "email": "[email protected]" },
    { "id": "2", "name": "Jane Smith", "email": "[email protected]" }
  ],
  "posts": [
    { "id": "1", "title": "Hello World", "authorId": "1" },
    { "id": "2", "title": "Another Post", "authorId": "2" }
  ],
  "comments": [
    { "id": "1", "text": "Great post!", "postId": "1" }
  ]
}
// json-server.config.js
module.exports = {
  port: 3001,
  watch: true,
  delay: 100,
  routes: 'routes.json',
};
// routes.json
{
  "/api/*": "/$1",
  "/users/:id/posts": "/posts?authorId=:id"
}
自定义路由
// server.js
const jsonServer = require('json-server');
const server = jsonServer.create();
const router = jsonServer.router('db.json');
const middlewares = jsonServer.defaults();

// Custom middleware for auth
server.use((req, res, next) => {
  if (req.headers.authorization !== 'Bearer valid-token') {
    if (req.method !== 'GET') {
      return res.status(401).json({ error: 'Unauthorized' });
    }
  }
  next();
});

server.use(middlewares);

// Custom routes
server.get('/api/me', (req, res) => {
  res.json({ id: '1', name: 'Current User', email: '[email protected]' });
});

server.post('/api/login', (req, res) => {
  const { email, password } = req.body;
  if (email === '[email protected]' && password === 'password') {
    res.json({ token: 'valid-token', user: { id: '1', email } });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

server.use('/api', router);

server.listen(3001, () => {
  console.log('Mock API running on http://localhost:3001');
});

测试数据工厂

// mocks/factories/user.ts
import { faker } from '@faker-js/faker';

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
  createdAt: string;
}

export function createUser(overrides: Partial<User> = {}): User {
  return {
    id: faker.string.uuid(),
    name: faker.person.fullName(),
    email: faker.internet.email(),
    role: 'user',
    createdAt: faker.date.past().toISOString(),
    ...overrides,
  };
}

export function createUsers(count: number, overrides: Partial<User> = {}): User[] {
  return Array.from({ length: count }, () => createUser(overrides));
}

// mocks/factories/index.ts
export * from './user';
export * from './post';
export * from './comment';
在测试中使用工厂
// tests/components/UserProfile.test.tsx
import { createUser } from '../mocks/factories';
import { server } from '../mocks/server';
import { http, HttpResponse } from 'msw';

describe('UserProfile', () => {
  it('displays admin badge for admin users', async () => {
    const adminUser = createUser({ role: 'admin', name: 'Admin User' });

    server.use(
      http.get('/api/users/:id', () => {
        return HttpResponse.json(adminUser);
      })
    );

    render(<UserProfile userId={adminUser.id} />);

    await waitFor(() => {
      expect(screen.getByText('Admin User')).toBeInTheDocument();
      expect(screen.getByTestId('admin-badge')).toBeInTheDocument();
    });
  });
});

基于场景的模拟

// mocks/scenarios.ts
import { http, HttpResponse, delay } from 'msw';
import { createUser, createUsers } from './factories';

export const scenarios = {
  default: [],

  emptyState: [
    http.get('/api/users', () => HttpResponse.json([])),
    http.get('/api/posts', () => HttpResponse.json([])),
  ],

  loadingState: [
    http.get('/api/users', async () => {
      await delay('infinite');
      return HttpResponse.json([]);
    }),
  ],

  errorState: [
    http.get('/api/users', () => {
      return new HttpResponse(null, { status: 500 });
    }),
  ],

  largeDataset: [
    http.get('/api/users', () => {
      return HttpResponse.json(createUsers(1000));
    }),
  ],

  slowNetwork: [
    http.get('/api/*', async ({ request }) => {
      await delay(2000);
      // Continue to default handler
      return undefined;
    }),
  ],
};

// Usage in tests
describe('UserList scenarios', () => {
  it('empty state', async () => {
    server.use(...scenarios.emptyState);
    // Test empty state
  });

  it('error state', async () => {
    server.use(...scenarios.errorState);
    // Test error handling
  });
});

// Usage in Storybook
export const EmptyState: Story = {
  parameters: {
    msw: {
      handlers: scenarios.emptyState,
    },
  },
};

Playwright 集成

// tests/e2e/api-mocking.spec.ts
import { test, expect } from '@playwright/test';

test.describe('API Mocking', () => {
  test('mock successful response', async ({ page }) => {
    await page.route('/api/users', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: '1', name: 'Mocked User', email: '[email protected]' },
        ]),
      });
    });

    await page.goto('/users');
    await expect(page.getByText('Mocked User')).toBeVisible();
  });

  test('mock error response', async ({ page }) => {
    await page.route('/api/users', async (route) => {
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: 'Server Error' }),
      });
    });

    await page.goto('/users');
    await expect(page.getByText('Error loading users')).toBeVisible();
  });

  test('delay response', async ({ page }) => {
    await page.route('/api/users', async (route) => {
      await new Promise((resolve) => setTimeout(resolve, 3000));
      await route.fulfill({
        status: 200,
        body: JSON.stringify([]),
      });
    });

    await page.goto('/users');
    await expect(page.getByTestId('loading-spinner')).toBeVisible();
  });
});

最佳实践

  1. 使用工厂:生成逼真的数据
  2. 定义场景:可复用的模拟配置
  3. 测试边界情况:错误、空状态、加载状态
  4. 保持处理器简单:每个处理器只负责一项职责
  5. 匹配生产 API:使用相同的契约
  6. 设置适当延迟:模拟真实耗时
  7. 记录模拟配置:API 契约
  8. 在测试之间重置:确保状态干净

输出检查清单

每个 API 模拟配置都应包含:

  • MSW/json-server 配置
  • 处理器定义
  • 测试设置集成
  • 测试数据工厂
  • 错误场景
  • 加载状态
  • GraphQL 支持(如有需要)
  • 浏览器集成
  • E2E 测试模拟
  • 文档

评论

支持 Markdown