バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

WireMockを使って外部サービスとの連携をモックする

こんにちは、開発2部の早瀬です。普段はリユースプラットフォームの出品管理SaaSの開発を行っています。

私が関わっているプロダクトでは外部サービスとの連携が非常に多く、開発環境ではそれらとの連携をモックしたいということがありました。そこで、WireMockを使用して開発環境にスタブサーバーを構築することで、外部サービスとの連携をモックすることができたので、今回はその方法と実践的なWireMockの使い方を紹介したいと思います!

背景

出品管理SaaSでは同一の商品を複数ECサイトへ同時出品するというのが主な機能になります。その際に、それぞれのECサイトとはAPIで連携をすることになります。それぞれの連携先でAPI連携の開発を目的としたテスト環境を提供してもらえるのですが、基本的にテスト環境は1つしか提供してもらえません。

私のチームでは本番環境、ステージング環境、開発環境の3つを設けています。自社プロダクトの本番環境は連携先の本番環境と、ステージング環境は連携先のテスト環境とそれぞれ疎通しています。
前述の通り、提供される外部サービスのテスト環境は1つのみで、これをステージング環境と開発環境の両方から使用するとデータ不整合のリスクがあります。

そのため、開発環境は連携先と疎通を持たない状態にしていましたが、その結果としてAPI連携関連の処理はエラーとなり、開発環境が実質的に使用できない状態となっていました。

そこで、WireMockを使用してスタブサーバーを構築し、開発環境ではそのスタブサーバーと疎通させることにしました。

WireMockとは

WireMockはスタブサーバーを簡単に構築できるOSSです。Javaアプリケーションとして機能する他、JSONファイルでモックの定義ができます。

https://wiremock.org

主に下記の理由から採用することにしました。

  • JSONでモックを定義できるので、コードを書く必要がない
  • Request MatchingResponse Templatingなどの機能により柔軟にモックを定義できる
  • JSON、XML、GraphQLのモックが必要で、それら全てに対応できる
  • Dockerイメージが配布されている

基本的な使い方

今回はDockerを用いた場合を例にして紹介します。

ディレクトリ構成

WireMockは/home/wiremockというルートディレクトリから、mappings__filesという2つのサブディレクトリを参照します。それぞれのディレクトリの役割は下記になります。

mappings

  • モックを定義したJSONファイルを格納する

__files

  • モックのレスポンスの定義ファイルを格納する
  • mappings配下のJSONファイル内でもレスポンスの定義は可能
  • レスポンスを別ファイルに切り出したい場合に使用する

そこで、私たちのプロジェクトでは、以下のようなディレクトリ構成を採用しています。

.
├── Dockerfile
├── Makefile
├── docker-compose.yaml
└── wiremock
    ├── mappings 
    └── __files

そのため、プロジェクトのwiremock配下を/home/wiremockにマウントするように、Dockerfiledocker-compose.yamlを定義します。

FROM wiremock/wiremock:2.35.0

COPY ./wiremock /home/wiremock

CMD ["--global-response-templating"]
Dockerfile


version: "3.8"
services:
  stub:
    build:
      context: .
    ports:
      - 8080:8080
    volumes:
      - ./wiremock:/home/wiremock
docker-compose.yaml

モックの定義

/sampleというパスに対するPOSTリクエストをモックする場合、JSONファイルは下記のようになります。

レスポンスを別ファイルに分ける場合

{
  "request": {
    "urlPath": "/sample",
    "method": "POST"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "bodyFileName": "response.json"
  }
}
wiremock/mappings/sample.json


{
  "foo": "bar",
  "nested": {
    "foo": "bar"
  }
}
wiremock/__files/response.json


レスポンスを別ファイルに分けない場合

{
  "request": {
    "urlPath": "/sample",
    "method": "POST"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "foo": "bar",
      "nested": {
        "foo": "bar"
      }
    }
  }
}
wiremock/mappings/sample.json

実践的なユースケース

ここからは、実際にスタブサーバーを構築する過程で遭遇したユースケースを紹介します。

クエリパラメータを含む場合

urlPathでパスを指定した場合はパスが完全一致していないといけません。そのため、クエリパラメータが含まれる場合はurlPathではなく、urlPathPatternで指定する必要があります。

pageというクエリパラメータに含まれる場合の例

{
  "request": {
    "urlPathPattern": "/sample",
    "method": "GET",
    "queryParameters": {
      "page": {
        "matches": ".*"
      }
    }
  },
  "response": {
    ...省略
  }
}

レスポンスを動的に返す

レスポンスを固定値ではなく、動的に返したい場合もあります。その場合はResponse Templatingという機能を使うことで実現できます。 二重中括弧({{}})で囲むことで、ifなどの条件式をはじめとする様々なヘルパーを使用することができます。
Response Templatingの機能はデフォルトでoffになっているので、使用する場合はコンテナ起動時にオプションを渡す必要があります。

CMD ["--global-response-templating"]

今回は使用場面が多かったヘルパーをいくつか紹介します。かなり多くのヘルパーが提供されているので、興味のある方は公式ドキュメントを参照してください。

ランダムな値を返す例

{
  "request": {
    ...省略
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "random": "{{randomValue length=10 type='ALPHANUMERIC'}}"
    }
  }
}
curl --request POST \
  --url http://localhost:8080/sample \
  --header 'Content-Type: application/json'

{"random":"jiw5kmpfew"}

リクエストの値を使用してレスポンスを生成する例

下記はリクエストボディのitemsというオブジェクトの配列を操作して、レスポンスを返却する例です。

{
  "request": {
    ...省略
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "body": "{ \"results\": [{{#each (jsonPath request.body '$.items') as |item|}}{\"key\": \"key {{item.id}}\"}{{#unless @last}},{{/unless}}{{/each}}] }"
  }
}
curl --request POST \
  --url http://localhost:8080/sample \
  --header 'Content-Type: application/json' \
  --data '{"items": [{"id": 1},{"id": 2}]}'

{ "results": [{"key": "key 1"},{"key": "key 2"}] }

ファイルを分割した上で動的にレスポンスを返す

レスポンスのファイルを分割した場合でもResponse Templatingは使用できます。ただし、途中に評価式が入るとレスポンスに不要な改行が含まれてしまいます。その場合は、trimヘルパーやチルダ(~)を使用して空行を取り除く必要があります。

trimヘルパーは先頭と末尾から空白・空行を削除します。

チルダは中括弧内部の最初({{~)か最後(~}})に入れることができます。最初に入れた場合はテンプレートブロックの直前の空白・空行が削除され、最後に入れた場合はテンプレートブロックの直後の空白・空行が削除されます。

空行を削除しない場合

レスポンスのファイル内でeachヘルパーを使用すると、eachブロックの前後と、eachブロック内で繰り返しごとに空行が生成されてしまいます。

{
  "results": [
    {{#each (jsonPath request.body '$.items') as |item|}}
    {"key": "key {{item.id}}"}{{#unless @last}},{{/unless}}
    {{/each}}
  ]
}
curl --request POST \
  --url http://localhost:8080/sample \
  --header 'Content-Type: application/json' \
  --data '{"items": [{"id": 1},{"id": 2}]}'

{
  "results": [
    
    {"key": "key 1"},
    
    {"key": "key 2"}
    
  ]
}

trimヘルパーとチルダを使用した場合

trimヘルパーを使用してeachブロックの前後の空行を、チルダを使用してeachブロック内の要素間の空行を削除します。

{
  "results": [
    {{#trim}}
    {{#each (jsonPath request.body '$.items') as |item|}}
    {"key": "key {{item.id}}"}{{#unless @last}},{{/unless}}
    {{~/each}}
    {{/trim}}
  ]
}
curl --request POST \
  --url http://localhost:8080/sample \
  --header 'Content-Type: application/json' \
  --data '{"items": [{"id": 1},{"id": 2}]}'

{
  "results": [
    {"key": "key 1"},
    {"key": "key 2"}
  ]
}

GraphQLのモック

GraphQLの場合、RESTとは違いエンドポイントが一つなので、エンドポイントごとにモックを定義することができません。私のチームではGraphQLのエンドポイントに対してリクエストを送る際は、ベストプラクティスに従い、オペレーション名を定義するようにしています。
そのためbodyPatternsを使用して、リクエストボディのoperationNameごとにモックを定義するようにしました。

{
  "request": {
    "url": "/sample/graphql",
    "method": "POST",
    "bodyPatterns": [
      {
        "matchesJsonPath": {
          "expression": "$.operationName",
          "equalTo": "SampleQuery"
        }
      }
    ]
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "application/json"
    },
    "jsonBody": {
      "data": {
        "item": {
          "id": 1
        }
      }
    }
  }
}
curl --request POST \
  --url http://localhost:8080/sample/graphql \
  --header 'Content-Type: application/json' \
  --data '{"query":"query SampleQuery {\n  item {\n\t\tid\n\t}\n}\n","operationName":"SampleQuery","variables":{}}'

{"data":{"item":{"id":1}}}

XMLのモック

XMLの場合、jsonBodyのようなXMLのレスポンスを返すヘルパーは用意されていません。そのため、レスポンスとなるXMLファイルを作成して、bodyFileNameで指定する必要があります。
また、JSONのリクエスト同様、Response TemplatingでリクエストのXMLを利用することもできます。

{
  "request": {
    "urlPath": "/sample",
    "method": "POST"
  },
  "response": {
    "status": 200,
    "headers": {
      "Content-Type": "text/xml"
    },
    "bodyFileName": "response.xml"
  }
}
<?xml version="1.0" encoding="UTF-8"?>
<results>
  {{#trim}}
  {{#each (xPath request.body '/items/item') as |item|}}
  <result>
    <key>key {{item.text}}</key>
  </result>
  {{~/each}}
  {{/trim}}
</results>
curl --request POST \                
  --url http://localhost:8080/sample \
  --header 'Content-Type: application/xml' \
  --data '<items><item><id>1</id></item><item><id>2</id></item></items>'

<?xml version="1.0" encoding="UTF-8"?>
<results>
  <result>
    <key>key 1</key>
  </result>
  <result>
    <key>key 2</key>
  </result>
</results>

WireMockではモック定義が難しい場合

今回は上記で紹介した方法で全てのAPIのモックを定義できました。しかし、今後も連携する外部サービスは増えていくので、WireMockではモック定義が難しいAPIが出てくる可能性もあります。そのため、そのようなAPIに対する対応方法も検討しました。

具体的には、WireMockでモック定義が難しいAPIの場合は、Proxingという機能を使用することにしました。
この機能を使用することで、WireMockが受け付けたリクエストを別のホストにプロキシし、そのホストから返ってきたレスポンスを呼び出し側に返すことができます。これにより、好きな言語でスタブサーバーを構築することができるので、かなり柔軟にレスポンスを定義することができます。

{
  "request": {
    "urlPath": "/sample",
    "method": "POST"
  },
  "response": {
    "proxyBaseUrl": "http://otherhost.com/sample"
  }
}

ただし、サーバーを建てるのもコストがかかりますし、WireMockを採用した理由の1つであるJSONでモックを簡単に定義できるというメリットが失われてしまいます。そのため、基本的にはResponse Templatingなどを活用しWireMock内で完結できるようにして、どうしてもWireMockの機能だけではモックを定義できない場合のみ、Proxingの機能を使用することにしました。

まとめ

今回はWireMockを使用したスタブサーバーの構築を、実践的なユースケースを交えて紹介しました。モックが必要なAPIが多岐にわたっていたのですが、WireMockの機能が豊富で、想像以上に簡単にスタブサーバーを構築することができました!
外部サービスとの連携があるプロダクトを開発している方には非常におすすめなので、ぜひ使ってみてください。

最後に、バイセルではエンジニアを随時募集しております。興味のある方はぜひ以下の採用サイトをご覧ください。

herp.careers