mcpField
A
api-mock-server

api-mock-server

github.com/patricio0312revテスト・品質63287 回インストール41 週間前に更新
リポジトリ

api-mock-server について

MSW、json-server、またはカスタムルートで開発・テスト用 API を作成するスキルです。フィクスチャや成功・エラーのシナリオに加え、ブラウザーやテストスイートとの連携を扱います。

主な機能

  1. 01MSW と json-server の設定
  2. 02ハンドラーとフィクスチャの定義
  3. 03成功・エラーシナリオの作成
  4. 04ブラウザー、単体、E2E テストとの連携
  5. 05GraphQL 操作のモック

活用シーン

  1. 01開発中のバックエンド API の再現
  2. 02固定または生成テストデータの準備
  3. 03エラー、空状態、読み込み状態の検証
  4. 04E2E テストでの API リクエスト捕捉

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/api-mock-serverを入力して読み込まれたか確認します。

詳細

API モックサーバー

テストと開発に使える、現実的なモック 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 テストのモック
  • ドキュメント

コメント