Skip to content
Published on

モックサーバー構築完全ガイド:WireMock・MSW・json-server・Prism実践

Authors

はじめに

モックサーバーは、バックエンドの準備が整っていない段階でもフロントエンド開発を進めたり、外部 API への依存を排除してテストを実行したりするために欠かせないツールです。本ガイドでは WireMock・MSW・json-server・Prism の4つの主要ツールを実践的に解説します。


1. モックサーバーとは:スタブ・フェイク・モックの違い

テストダブルにはいくつかの種類があります。

種類説明
スタブ (Stub)決まったレスポンスを返すだけGET /users/1 → 固定 JSON
フェイク (Fake)動作する簡易実装インメモリ DB
モック (Mock)呼び出しを検証できるスタブWireMock の verify
スパイ (Spy)実装を持ちつつ呼び出しを記録Mockito の @Spy

モックサーバーは主に「スタブ」と「モック」の機能を提供し、HTTP レイヤーで外部 API をエミュレートします。

モックサーバーを使う理由

  • バックエンドが未完成でもフロントエンド開発を並行できる
  • 外部 API の障害・レート制限・コストを回避できる
  • エラーケース・遅延などの再現が困難なシナリオをテストできる
  • CI/CD 環境で外部依存なしにテストを実行できる

2. WireMock スタンドアロン

JAR を使った起動

WireMock はスタンドアロン JAR として起動できます。

# JAR のダウンロード
curl -o wiremock.jar \
  https://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-jre8-standalone/2.35.0/wiremock-jre8-standalone-2.35.0.jar

# 起動 (デフォルトポート 8080)
java -jar wiremock.jar

# カスタムポートで起動
java -jar wiremock.jar --port 9000 --https-port 9443

# レコーディングモード(実際の API をプロキシしてレスポンスを記録)
java -jar wiremock.jar --proxy-all "https://api.example.com" --record-mappings

mappings フォルダによる JSON 設定

WireMock は mappings/ フォルダに JSON ファイルを置くだけでスタブを定義できます。

wiremock/
  mappings/
    get-users.json
    post-order.json
    get-product-by-id.json
  __files/
    users-response.json
    order-response.json
// mappings/get-users.json
{
  "request": {
    "method": "GET",
    "url": "/api/users"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "bodyFileName": "users-response.json"
  }
}
// mappings/get-product-by-id.json(URL パターンマッチング)
{
  "request": {
    "method": "GET",
    "urlPathPattern": "/api/products/[0-9]+"
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "body": "{\"id\": 1, \"name\": \"サンプル商品\", \"price\": 1500}"
  }
}

動的レスポンス(テンプレート)

// mappings/dynamic-response.json
{
  "request": {
    "method": "POST",
    "url": "/api/echo"
  },
  "response": {
    "status": 200,
    "headers": { "Content-Type": "application/json" },
    "body": "{ \"received\": \"{{jsonPath request.body '$.message'}}\" }",
    "transformers": ["response-template"]
  }
}
クイズ1: WireMock のスタンドアロンモードとライブラリモードの違いは?

答え: スタンドアロンモードは独立した JAR プロセスとして起動し、どの言語からでも利用できます。ライブラリモードは Java プロジェクトに依存として追加し、テストコード内でプログラム的に制御します。

解説: スタンドアロンモードは JSON ファイルで設定を管理するため、フロントエンド開発者が Java を知らなくても使えるメリットがあります。ライブラリモードはテストコードと設定を同じ言語で管理でき、動的なスタブ生成が容易です。チームの構成や用途によって使い分けます。


3. WireMock Java: JUnit5 との統合

@WireMockTest アノテーション

import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import static com.github.tomakehurst.wiremock.client.WireMock.*;

@WireMockTest(httpPort = 8089)
class PaymentApiClientTest {

    private PaymentApiClient client;

    @BeforeEach
    void setUp() {
        client = new PaymentApiClient("http://localhost:8089");
    }

    @Test
    void chargeCard_success_returnsTransactionId() {
        stubFor(post(urlEqualTo("/payments/charge"))
            .withRequestBody(matchingJsonPath("$.amount", equalTo("5000")))
            .withRequestBody(matchingJsonPath("$.currency", equalTo("JPY")))
            .withHeader("Authorization", matching("Bearer .+"))
            .willReturn(aResponse()
                .withStatus(201)
                .withHeader("Content-Type", "application/json")
                .withBody("{\"transactionId\": \"txn_abc123\", \"status\": \"SUCCESS\"}")));

        String txnId = client.charge(5000, "JPY", "Bearer token123");

        assertThat(txnId).isEqualTo("txn_abc123");
        verify(postRequestedFor(urlEqualTo("/payments/charge")));
    }

    @Test
    void chargeCard_networkError_retriesThreeTimes() {
        stubFor(post(urlEqualTo("/payments/charge"))
            .willReturn(aResponse()
                .withFault(Fault.CONNECTION_RESET_BY_PEER)));

        assertThatThrownBy(() -> client.charge(1000, "JPY", "Bearer token"))
            .isInstanceOf(PaymentNetworkException.class);

        verify(3, postRequestedFor(urlEqualTo("/payments/charge")));
    }
}

WireMockExtension を使った細かい制御

@ExtendWith(WireMockExtension.class)
class AdvancedWireMockTest {

    @RegisterExtension
    static WireMockExtension wm = WireMockExtension.newInstance()
        .options(wireMockConfig()
            .port(8090)
            .httpsPort(8091)
            .withRootDirectory("src/test/resources/wiremock"))
        .build();

    @Test
    void testWithCustomConfig() {
        wm.stubFor(get("/api/data")
            .willReturn(okJson("{\"key\": \"value\"}")));

        // テスト実行後のリクエスト履歴を取得
        List<LoggedRequest> requests = wm.findAll(getRequestedFor(anyUrl()));
        assertThat(requests).isNotEmpty();
    }
}
クイズ2: WireMock の Fault シミュレーションはいつ使うべきか?

答え: ネットワーク障害(接続リセット、タイムアウト、不正なレスポンス)に対するクライアントの耐性をテストしたい場合に使います。

解説: Fault.CONNECTION_RESET_BY_PEER(接続リセット)、Fault.EMPTY_RESPONSE(空レスポンス)、Fault.MALFORMED_RESPONSE_CHUNK(不正チャンク)などを使えます。これにより「本番環境で稀に起きる障害」を再現し、リトライロジックやフォールバックが正しく機能するかを検証できます。


4. MSW(Mock Service Worker)

ブラウザ環境のセットアップ

MSW はサービスワーカーを使ってブラウザ上で HTTP リクエストをインターセプトします。

npm install msw --save-dev
npx msw init public/ --save
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw'

export const handlers = [
  // GET リクエストのハンドラー
  http.get('/api/users', () => {
    return HttpResponse.json([
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' },
    ])
  }),

  // パスパラメータを使ったハンドラー
  http.get('/api/users/:id', ({ params }) => {
    const { id } = params
    if (id === '99') {
      return new HttpResponse(null, { status: 404 })
    }
    return HttpResponse.json({ id: Number(id), name: `User ${id}` })
  }),

  // POST リクエストのハンドラー
  http.post('/api/users', async ({ request }) => {
    const body = (await request.json()) as { name: string; email: string }
    return HttpResponse.json({ id: Date.now(), ...body }, { status: 201 })
  }),

  // 認証エラーのシミュレーション
  http.get('/api/secure', ({ request }) => {
    const auth = request.headers.get('Authorization')
    if (!auth?.startsWith('Bearer ')) {
      return new HttpResponse(null, { status: 401 })
    }
    return HttpResponse.json({ data: 'secret' })
  }),
]
// src/mocks/browser.ts
import { setupWorker } from 'msw/browser'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)
// src/main.tsx(開発環境でのみ有効化)
async function enableMocking() {
  if (process.env.NODE_ENV !== 'development') {
    return
  }
  const { worker } = await import('./mocks/browser')
  return worker.start({
    onUnhandledRequest: 'bypass',  // 未定義リクエストはそのまま通す
  })
}

enableMocking().then(() => {
  ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
})

Node.js 環境(テスト)でのセットアップ

// src/mocks/server.ts
import { setupServer } from 'msw/node'
import { handlers } from './handlers'

export const server = setupServer(...handlers)
// vitest.setup.ts または jest.setup.ts
import { server } from './src/mocks/server'

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
// UserList.test.tsx
import { render, screen, waitFor } from '@testing-library/react'
import { server } from '../mocks/server'
import { http, HttpResponse } from 'msw'
import UserList from './UserList'

test('ユーザー一覧が表示される', async () => {
  render(<UserList />)

  await waitFor(() => {
    expect(screen.getByText('Alice')).toBeInTheDocument()
    expect(screen.getByText('Bob')).toBeInTheDocument()
  })
})

test('APIエラー時にエラーメッセージが表示される', async () => {
  // テスト固有のハンドラーでオーバーライド
  server.use(
    http.get('/api/users', () => {
      return new HttpResponse(null, { status: 500 })
    })
  )

  render(<UserList />)

  await waitFor(() => {
    expect(screen.getByText('データの取得に失敗しました')).toBeInTheDocument()
  })
})
クイズ3: MSW がサービスワーカーを使う利点は何か?

答え: サービスワーカーはネットワーク層でリクエストをインターセプトするため、アプリケーションコードを変更せずにモックを注入できます。テスト環境と本番環境で同じコードが動作します。

解説: 従来の fetchaxios をモックする方法はアプリケーションコードに密結合します。MSW はブラウザのサービスワーカー API を利用するため、実際の HTTP リクエストと同じ流れで処理され、より現実に近いテストが可能です。また、server.use() でテストごとにハンドラーを動的に変更でき、server.resetHandlers() でリセットできるため、テスト間の干渉を防げます。


5. json-server

基本セットアップ

npm install -g json-server
# または開発依存として
npm install --save-dev json-server
// db.json
{
  "users": [
    { "id": 1, "name": "Alice", "email": "alice@example.com", "role": "admin" },
    { "id": 2, "name": "Bob", "email": "bob@example.com", "role": "user" }
  ],
  "products": [
    { "id": 1, "name": "Java入門", "price": 3000, "categoryId": 1 },
    { "id": 2, "name": "Spring Boot実践", "price": 4500, "categoryId": 1 },
    { "id": 3, "name": "React完全ガイド", "price": 3800, "categoryId": 2 }
  ],
  "categories": [
    { "id": 1, "name": "技術書" },
    { "id": 2, "name": "フロントエンド" }
  ],
  "orders": []
}
# 起動(デフォルトポート 3000)
json-server --watch db.json

# カスタムポートで起動
json-server --watch db.json --port 4000

# 遅延を追加(ms)
json-server --watch db.json --delay 500

json-server が自動生成するエンドポイント:

  • GET /users - 全ユーザー取得
  • GET /users/1 - ID 指定取得
  • POST /users - 新規作成
  • PUT /users/1 - 全体更新
  • PATCH /users/1 - 部分更新
  • DELETE /users/1 - 削除
  • GET /users?role=admin - フィルタリング
  • GET /users?_sort=name&_order=asc - ソート
  • GET /users?_page=1&_limit=10 - ページネーション

routes.json によるカスタムルーティング

// routes.json
{
  "/api/*": "/$1",
  "/api/v2/users/:id": "/users/:id",
  "/blog/:year/:month": "/posts?year=:year&month=:month"
}
json-server --watch db.json --routes routes.json

カスタムミドルウェア

// middleware.js
module.exports = (req, res, next) => {
  // 認証チェック
  if (req.path.startsWith('/api/secure') && !req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' })
  }

  // レスポンスにメタ情報を追加
  res.header('X-Powered-By', 'json-server-mock')

  // POST リクエストにタイムスタンプを追加
  if (req.method === 'POST') {
    req.body.createdAt = new Date().toISOString()
  }

  next()
}
json-server --watch db.json --middlewares middleware.js

6. Prism: OpenAPI 仕様からの自動モック生成

セットアップと起動

npm install -g @stoplight/prism-cli
# openapi.yaml
openapi: 3.0.0
info:
  title: User API
  version: 1.0.0
paths:
  /users:
    get:
      summary: ユーザー一覧取得
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
              example:
                - id: 1
                  name: Alice
                  email: alice@example.com
  /users/{id}:
    get:
      summary: ユーザー取得
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: 成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: Not Found
components:
  schemas:
    User:
      type: object
      required: [id, name, email]
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
# モックサーバーを起動
prism mock openapi.yaml

# バリデーション付きプロキシモード
prism proxy openapi.yaml https://api.example.com

# 動的データ生成(faker を使用)
prism mock openapi.yaml --dynamic

Prism の動作モード

  • 静的モード: example フィールドに定義したデータを返す
  • 動的モード: スキーマから自動でランダムデータを生成する
  • プロキシモード: 実際の API へのリクエストをバリデーションしながら中継する
クイズ4: Prism のプロキシモードはどんな場面で役立つか?

答え: バックエンド API が開発中であっても OpenAPI 仕様が先に定義されている場合に、フロントエンドは仕様通りのレスポンスを受け取りながら開発を進められます。また、本番 API へのリクエストが仕様に準拠しているかを検証するためにも使えます。

解説: プロキシモードでは、リクエストとレスポンスの両方が OpenAPI スキーマに対して検証されます。仕様からの逸脱(未定義フィールド、型の不一致など)があれば警告やエラーが出力されます。これにより、フロントエンドとバックエンドの「契約」を継続的に確認できます。


7. フロントエンド開発でのモックサーバー活用パターン

パターン 1: 開発環境専用モック(MSW)

// vite.config.ts の環境変数で切り替え
// .env.development
VITE_USE_MOCK=true

// src/main.tsx
const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true'

async function bootstrap() {
  if (USE_MOCK) {
    const { worker } = await import('./mocks/browser')
    await worker.start()
  }
  ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
}

パターン 2: シナリオ切り替え

// 開発中にUIでシナリオを切り替える
const scenarios = {
  normal: handlers,
  loading: [
    http.get('/api/users', async () => {
      await delay(Infinity) // 永続的なローディング
      return HttpResponse.json([])
    }),
  ],
  error: [
    http.get('/api/users', () => {
      return new HttpResponse(null, { status: 500 })
    }),
  ],
  empty: [
    http.get('/api/users', () => {
      return HttpResponse.json([])
    }),
  ],
}

// DevTools でシナリオを切り替え
window.__MOCK_SCENARIO__ = 'error'
server.use(...scenarios[window.__MOCK_SCENARIO__])

パターン 3: Storybook との統合

// .storybook/preview.ts
import { initialize, mswLoader } from 'msw-storybook-addon'

initialize()

export const preview = {
  loaders: [mswLoader],
}

// UserCard.stories.tsx
export const WithError: Story = {
  parameters: {
    msw: {
      handlers: [
        http.get('/api/users/1', () => {
          return new HttpResponse(null, { status: 404 })
        }),
      ],
    },
  },
}

8. CI/CD でのモックサーバー統合

GitHub Actions での WireMock

# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Java
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'

      - name: Start WireMock
        run: |
          docker run -d \
            --name wiremock \
            -p 8080:8080 \
            -v $PWD/wiremock:/home/wiremock \
            wiremock/wiremock:3.3.1

      - name: Wait for WireMock
        run: |
          until curl -sf http://localhost:8080/__admin/; do
            echo "Waiting for WireMock..."
            sleep 1
          done

      - name: Run tests
        run: ./mvnw test -Dapi.base.url=http://localhost:8080

      - name: Stop WireMock
        if: always()
        run: docker stop wiremock

Docker Compose でのモック環境

# docker-compose.test.yml
version: '3.8'

services:
  app:
    build: .
    environment:
      - PAYMENT_API_URL=http://payment-mock:8080
      - EMAIL_API_URL=http://email-mock:8080
    depends_on:
      - payment-mock
      - email-mock

  payment-mock:
    image: wiremock/wiremock:3.3.1
    volumes:
      - ./mocks/payment:/home/wiremock
    ports:
      - '8081:8080'

  email-mock:
    image: wiremock/wiremock:3.3.1
    volumes:
      - ./mocks/email:/home/wiremock
    ports:
      - '8082:8080'

  test-runner:
    build:
      context: .
      dockerfile: Dockerfile.test
    depends_on:
      - app
    command: npm test

json-server を CI で使う

// package.json
{
  "scripts": {
    "mock": "json-server --watch db.json --port 4000",
    "test:e2e": "start-server-and-test mock http://localhost:4000 cypress:run"
  },
  "devDependencies": {
    "json-server": "^1.0.0",
    "start-server-and-test": "^2.0.0",
    "cypress": "^13.0.0"
  }
}
クイズ5: Contract Testing(コントラクトテスト)とモックサーバーの関係は?

答え: コントラクトテストは、コンシューマー(クライアント)とプロバイダー(サーバー)間の「契約(API仕様)」を自動的に検証します。モックサーバーはコンシューマー側のテストで使われ、プロバイダー側でその契約が本当に満たされているかを検証します。

解説: Pact などのコントラクトテストツールを使うと、コンシューマーがモックサーバーとのやりとりから「コントラクトファイル」を生成し、そのファイルをプロバイダーが検証します。これにより、モックと実際の API の乖離(「モックが嘘をついている」問題)を防ぎます。CI パイプラインに組み込むと、API の変更がコンシューマーに影響を与えるかを自動で検出できます。


ツール比較まとめ

ツール言語/環境向いている用途設定方法
WireMockJava/JVMJava バックエンドテストJava コード / JSON
MSWBrowser/Node.jsReact/Vue テスト、StorybookTypeScript ハンドラー
json-serverNode.jsプロトタイピング、フロント開発db.json
Prism任意OpenAPI 仕様駆動開発YAML/JSON 仕様ファイル

選択指針

  • Java Spring Boot テスト → WireMock(JUnit5 統合が充実)
  • React/Vue フロントエンド開発 → MSW(ブラウザに最も近い動作)
  • 素早いプロトタイプ・デモ → json-server(最短で起動可能)
  • API ファースト開発 → Prism(OpenAPI 仕様から自動生成)
  • 複数マイクロサービスのテスト → WireMock スタンドアロン + Docker Compose

まとめ

モックサーバーを適切に活用することで、開発速度の向上とテスト品質の改善を同時に達成できます。フロントエンドとバックエンドの並行開発、外部 API への依存排除、エッジケースの再現など、様々なシナリオでモックサーバーは威力を発揮します。各ツールの特性を理解し、プロジェクトのニーズに合った選択をしましょう。