0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WireMock を Docker で動かして外部 API をスタブする

0
Posted at

はじめに

この記事では、WireMock を Docker で起動し、外部 API のスタブとして使う方法を紹介します。

JSON ファイルにスタブ定義を書いておくだけで、任意のエンドポイントに対して任意のレスポンスを返すサーバーを立ち上げられます。

この記事で紹介するサンプルプロジェクトは以下のリポジトリで公開しています。

WireMock とは

WireMock は HTTP のスタブ・モックサーバーです。リクエストの条件(URL・メソッド・ヘッダー・ボディなど)とレスポンスをセットで定義しておくと、条件に一致したリクエストに対して定義したレスポンスを返します。

# Docker で起動するだけ
docker compose up

OpenAPI 定義は不要です。スタブしたいエンドポイントを JSON で直接書きます。

なぜ WireMock を使うのか

外部 API に依存するシステムを開発・テストするとき、次のような場面に出くわします。

  • 外部 API がまだ開発中で、実際に叩ける環境がない
  • テストのたびに外部 API を叩くと、料金が発生したり、テストデータが汚染されたりする
  • タイムアウトや 500 エラーなど、外部 API の異常系を再現するのが難しい

WireMock を使うと、外部 API の代わりにスタブサーバーを立てられます。ローカルで動くため、外部 API の状態に左右されずにテストや開発を進められます。

Prism だと困る場面がある

Prism は OpenAPI ファイルからモックサーバーを立ち上げるツールです。OpenAPI を書けばすぐ動くので、フロントエンドの先行開発や設計確認には十分です。

一方で、外部 API のスタブとして細かい挙動を固定したい場面では、Prism より WireMock のほうが扱いやすいことがあります。たとえば次のような場面です。

  • 送ってきたリクエストの内容によってレスポンスを変えたい
  • レスポンスを意図的に遅らせてタイムアウトを再現したい
  • エラー系のレスポンスを毎回同じ条件で返したい
  • 呼び出し回数に応じて返す内容を変えたい

たとえば「外部の決済 API がエラーを返したとき、自分のアプリはちゃんとリトライするか」を確認したいとします。Prism でも確認自体はできますが、都度ヘッダーや example の切り替えを意識する必要があります。テストとして繰り返し実行するなら、WireMock のように条件をスタブとして固定できるほうが扱いやすいです。

WireMock は JSON でスタブを自分で書きますが、その分「このリクエストが来たらこれを返す」を細かく決められます。

WireMock Prism
設定方法 JSON でスタブを直接書く OpenAPI ファイルから自動生成
リクエスト内容で返すものを変える できる OpenAPI ベースの範囲で行う
エラー系を常時定義しておく できる 都度切り替えが必要になりやすい
レスポンスを遅らせる できる 運用用途では向きにくい
向いている場面 外部 API のスタブ、テスト フロント先行開発、API 設計確認

Prism の使い方は「OpenAPI と Prism でモックサーバーを立てる」で整理しています。サンプルプロジェクトは GitHub で公開しています。

サンプルの構成

この記事のサンプルでは次のエンドポイントを定義します。

エンドポイント 説明
GET /api/users ユーザー一覧取得(固定レスポンス)
GET /api/users/:id ユーザー個別取得(存在しないIDは404)
POST /api/users ユーザー作成(ボディ条件によって分岐)
GET /api/slow 遅延レスポンス(3秒)
GET /api/orders/1 ボディをファイルで管理

Docker で起動する

docker-compose.yml

services:
  wiremock:
    image: wiremock/wiremock:3.13.0
    ports:
      - "8080:8080"
    volumes:
      - ./wiremock/mappings:/home/wiremock/mappings:ro
      - ./wiremock/__files:/home/wiremock/__files:ro
    command:
      - "--verbose"
      - "--global-response-templating"

WireMock の公式 Docker イメージを使います。

ポイントは次のとおりです。

  • mappings/ にスタブ定義ファイル(JSON)を置く
  • __files/ にレスポンスボディファイルを置く
  • --global-response-templating を有効にすると、リクエストの値をレスポンスに埋め込めるようになる

起動・停止

# 起動
docker compose up

# バックグラウンドで起動
docker compose up -d

# 停止
docker compose down

起動すると http://localhost:8080 でリッスンします。

スタブを定義する

スタブは mappings/ 配下に JSON ファイルを置くだけで有効になります。ファイル名は何でも構いません。

基本的な定義(GET /api/users)

{
  "mappings": [
    {
      "name": "ユーザー一覧取得",
      "request": {
        "method": "GET",
        "url": "/api/users"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json"
        },
        "jsonBody": {
          "users": [
            { "id": 1, "name": "田中 太郎", "email": "tanaka@example.com" },
            { "id": 2, "name": "鈴木 花子", "email": "suzuki@example.com" }
          ]
        }
      }
    }
  ]
}

request でマッチ条件を指定し、response で返す内容を指定します。jsonBody に直接オブジェクトを書けば、Content-Type を application/json にした場合に JSON として返ります。

URL パターンマッチと条件分岐(GET /api/users/:id)

特定の ID だけ 404 を返す例です。条件が重なったときにどちらを優先するかが分かるよう、priority を明示しています。

{
  "mappings": [
    {
      "name": "ユーザー個別取得(存在するID)",
      "priority": 10,
      "request": {
        "method": "GET",
        "urlPattern": "/api/users/(?!999)[0-9]+"
      },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "jsonBody": {
          "id": 1,
          "name": "田中 太郎",
          "email": "tanaka@example.com"
        }
      }
    },
    {
      "name": "ユーザー個別取得(存在しないID: 999)",
      "priority": 1,
      "request": {
        "method": "GET",
        "url": "/api/users/999"
      },
      "response": {
        "status": 404,
        "headers": { "Content-Type": "application/json" },
        "jsonBody": {
          "error": "NOT_FOUND",
          "message": "指定されたユーザーが見つかりません"
        }
      }
    }
  ]
}

url は完全一致、urlPattern は正規表現でマッチします。複数のスタブが同時にマッチしうる場合は、priority を付けて優先順位を明示しておくほうが安全です。数値が小さいほうが優先されます。

リクエストボディの条件分岐(POST /api/users)

bodyPatternsmatchesJsonPath を組み合わせると、リクエストボディの内容によってレスポンスを変えられます。

{
  "mappings": [
    {
      "name": "ユーザー作成(正常系)",
      "request": {
        "method": "POST",
        "url": "/api/users",
        "bodyPatterns": [
          { "matchesJsonPath": "$.name" },
          { "matchesJsonPath": "$.email" }
        ]
      },
      "response": {
        "status": 201,
        "headers": { "Content-Type": "application/json" },
        "jsonBody": {
          "id": 3,
          "name": "{{jsonPath request.body '$.name'}}",
          "email": "{{jsonPath request.body '$.email'}}"
        }
      }
    },
    {
      "name": "ユーザー作成(バリデーションエラー: nameなし)",
      "request": {
        "method": "POST",
        "url": "/api/users",
        "bodyPatterns": [
          {
            "matchesJsonPath": {
              "expression": "$.name",
              "absent": true
            }
          }
        ]
      },
      "response": {
        "status": 400,
        "headers": { "Content-Type": "application/json" },
        "jsonBody": {
          "error": "VALIDATION_ERROR",
          "message": "name は必須です"
        }
      }
    }
  ]
}

正常系の response では {{jsonPath request.body '$.name'}} という記法でリクエストボディの値をレスポンスに埋め込んでいます。これは --global-response-templating を有効にしたときだけ使えます。

遅延レスポンス(GET /api/slow)

fixedDelayMilliseconds を指定するだけで、レスポンスを遅延させられます。

{
  "mappings": [
    {
      "name": "遅延レスポンス(タイムアウトテスト用)",
      "request": {
        "method": "GET",
        "url": "/api/slow"
      },
      "response": {
        "status": 200,
        "jsonBody": { "message": "3秒後に返ってきたレスポンス" },
        "fixedDelayMilliseconds": 3000
      }
    }
  ]
}

タイムアウト処理やリトライロジックの動作確認に使えます。

レスポンスボディをファイルで管理(GET /api/orders/1)

レスポンスボディが大きくなる場合、bodyFileName__files/ 配下のファイルを参照できます。

{
  "mappings": [
    {
      "name": "注文詳細取得",
      "request": {
        "method": "GET",
        "url": "/api/orders/1"
      },
      "response": {
        "status": 200,
        "headers": { "Content-Type": "application/json" },
        "bodyFileName": "order-1.json"
      }
    }
  ]
}

__files/order-1.json に実際のレスポンスボディを書きます。スタブ定義ファイルがすっきりし、複数のスタブから同じファイルを参照することもできます。

シナリオ機能:呼び出し回数で返すものを変える

Prism にはない機能の一つが Scenarios です。同じエンドポイントに対して「1回目はエラー、2回目は成功」という動きを定義できます。リトライ処理が正しく動くかを確認するのに使えます。

{
  "mappings": [
    {
      "name": "リトライシナリオ: 1回目はエラー",
      "scenarioName": "外部APIリトライ",
      "requiredScenarioState": "Started",
      "newScenarioState": "1回目失敗済み",
      "request": {
        "method": "GET",
        "url": "/api/external/payment"
      },
      "response": {
        "status": 503,
        "jsonBody": {
          "error": "SERVICE_UNAVAILABLE",
          "message": "サービスが一時的に利用できません"
        }
      }
    },
    {
      "name": "リトライシナリオ: 2回目は成功",
      "scenarioName": "外部APIリトライ",
      "requiredScenarioState": "1回目失敗済み",
      "request": {
        "method": "GET",
        "url": "/api/external/payment"
      },
      "response": {
        "status": 200,
        "jsonBody": {
          "paymentId": "PAY-001",
          "status": "COMPLETED",
          "amount": 5000
        }
      }
    }
  ]
}

scenarioName で同じグループに属することを示し、requiredScenarioState で「どの状態のときにこのスタブを使うか」を指定します。newScenarioState でリクエストを受け取った後の次の状態を指定します。

初期状態は Started です。最初のリクエストでシナリオが 1回目失敗済み に進み、次のリクエストで成功レスポンスが返ります。

シナリオをリセットして最初から試したいときは、管理 API を使います。

curl -s -X POST http://localhost:8080/__admin/scenarios/reset

障害シミュレーション:接続レベルのエラーを再現する

遅延だけでなく、「接続が突然切れる」「何も返ってこない」といった障害も再現できます。responsefault を指定するだけです。

{
  "mappings": [
    {
      "name": "接続リセット",
      "request": { "method": "GET", "url": "/api/fault/connection-reset" },
      "response": { "fault": "CONNECTION_RESET_BY_PEER" }
    },
    {
      "name": "空レスポンス",
      "request": { "method": "GET", "url": "/api/fault/empty" },
      "response": { "fault": "EMPTY_RESPONSE" }
    },
    {
      "name": "壊れたレスポンス",
      "request": { "method": "GET", "url": "/api/fault/malformed" },
      "response": { "fault": "MALFORMED_RESPONSE_CHUNK" }
    }
  ]
}

指定できる fault の値は次のとおりです。

再現する障害
CONNECTION_RESET_BY_PEER 接続が突然切断される
EMPTY_RESPONSE ヘッダーもボディも何も返ってこない
MALFORMED_RESPONSE_CHUNK 壊れたデータが返ってくる

これらはリトライ処理やサーキットブレーカーのロジックを手元で確認したいときに役立ちます。503 を返すだけでは再現できない、ネットワーク層のエラーを試せます。

動作確認

curl で確認する

requests/ 配下のシェルスクリプトをそのまま実行できます(jq が必要です)。

# ユーザー一覧・個別取得
bash requests/01_get-users.sh
bash requests/02_get-user-by-id.sh

# POST(正常系・バリデーションエラー)
bash requests/03_post-user.sh

# 遅延レスポンス(3秒かかります)
bash requests/04_slow-response.sh

# ボディをファイルで管理している注文詳細
bash requests/05_get-order.sh

# シナリオ(1回目503・2回目200・リセット)
bash requests/06_scenario-retry.sh

# 障害シミュレーション(接続リセット・空レスポンス・壊れたレスポンス)
bash requests/07_fault-simulation.sh

個別に試したい場合は次のとおりです。

# ユーザー一覧
curl -s http://localhost:8080/api/users | jq .

# ユーザー個別取得(存在するID)
curl -s http://localhost:8080/api/users/1 | jq .

# ユーザー個別取得(存在しないID → 404)
curl -s http://localhost:8080/api/users/999 | jq .

# ユーザー作成(正常系)
curl -s -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "山田 次郎", "email": "yamada@example.com"}' | jq .

# ユーザー作成(name なし → 400)
curl -s -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"email": "yamada@example.com"}' | jq .

# 遅延レスポンス
curl -s http://localhost:8080/api/slow | jq .

# シナリオ(1回目・2回目の順に実行)
curl -s http://localhost:8080/api/external/payment | jq .
curl -s http://localhost:8080/api/external/payment | jq .

# シナリオリセット
curl -s -X POST http://localhost:8080/__admin/scenarios/reset

# 障害シミュレーション(正常なJSONは返らないので -v で確認)
curl -v http://localhost:8080/api/fault/connection-reset
curl -v http://localhost:8080/api/fault/empty
curl -v http://localhost:8080/api/fault/malformed

VSCode REST Client

拡張機能 REST Client をインストールして requests/wiremock.http を開き、各リクエストの Send Request をクリックします。

WireMock 管理 API

登録済みスタブの確認やリクエストログの確認に使えます。

# 登録済みスタブ一覧
curl -s http://localhost:8080/__admin/mappings | jq .

# リクエスト受信ログ
curl -s http://localhost:8080/__admin/requests | jq .

# ログをリセット
curl -s -X DELETE http://localhost:8080/__admin/requests

ログを見ると「どのスタブがマッチしたか」「マッチしなかった場合の詳細」を確認できます。スタブが意図どおりに動いているか確認するときに役立ちます。

まとめ

WireMock は JSON ファイルにスタブ定義を書くだけで動く HTTP スタブサーバーです。

次の点が使いやすいと感じています。

  • OpenAPI 定義がなくても使える
  • URL パターン・ボディ条件など細かい条件分岐が書ける
  • 遅延レスポンスでタイムアウト系のテストが再現できる
  • シナリオ機能で「1回目エラー・2回目成功」などのリトライ検証ができる
  • 接続リセットや空レスポンスなど、ネットワーク層の障害も再現できる
  • 管理 API でリクエストログを確認できる

外部 API の異常系を再現したい場面や、テスト時に外部依存を切り離したい場面で選択肢に入れてみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?