こんにちは、開発2部の早瀬です。普段はリユースプラットフォームの出品管理SaaSの開発を行っています。
私が関わっているプロダクトでは外部サービスとの連携が非常に多く、開発環境ではそれらとの連携をモックしたいということがありました。そこで、WireMockを使用して開発環境にスタブサーバーを構築することで、外部サービスとの連携をモックすることができたので、今回はその方法と実践的なWireMockの使い方を紹介したいと思います!
背景
出品管理SaaSでは同一の商品を複数ECサイトへ同時出品するというのが主な機能になります。その際に、それぞれのECサイトとはAPIで連携をすることになります。それぞれの連携先でAPI連携の開発を目的としたテスト環境を提供してもらえるのですが、基本的にテスト環境は1つしか提供してもらえません。
私のチームでは本番環境、ステージング環境、開発環境の3つを設けています。自社プロダクトの本番環境は連携先の本番環境と、ステージング環境は連携先のテスト環境とそれぞれ疎通しています。
前述の通り、提供される外部サービスのテスト環境は1つのみで、これをステージング環境と開発環境の両方から使用するとデータ不整合のリスクがあります。
そのため、開発環境は連携先と疎通を持たない状態にしていましたが、その結果としてAPI連携関連の処理はエラーとなり、開発環境が実質的に使用できない状態となっていました。
そこで、WireMockを使用してスタブサーバーを構築し、開発環境ではそのスタブサーバーと疎通させることにしました。
WireMockとは
WireMockはスタブサーバーを簡単に構築できるOSSです。Javaアプリケーションとして機能する他、JSONファイルでモックの定義ができます。
主に下記の理由から採用することにしました。
- JSONでモックを定義できるので、コードを書く必要がない
- Request MatchingやResponse 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
にマウントするように、Dockerfile
とdocker-compose.yaml
を定義します。
FROM wiremock/wiremock:2.35.0 COPY ./wiremock /home/wiremock CMD ["--global-response-templating"]
version: "3.8" services: stub: build: context: . ports: - 8080:8080 volumes: - ./wiremock:/home/wiremock
モックの定義
/sample
というパスに対するPOSTリクエストをモックする場合、JSONファイルは下記のようになります。
レスポンスを別ファイルに分ける場合
{ "request": { "urlPath": "/sample", "method": "POST" }, "response": { "status": 200, "headers": { "Content-Type": "application/json" }, "bodyFileName": "response.json" } }
{ "foo": "bar", "nested": { "foo": "bar" } }
レスポンスを別ファイルに分けない場合
{ "request": { "urlPath": "/sample", "method": "POST" }, "response": { "status": 200, "headers": { "Content-Type": "application/json" }, "jsonBody": { "foo": "bar", "nested": { "foo": "bar" } } } }
実践的なユースケース
ここからは、実際にスタブサーバーを構築する過程で遭遇したユースケースを紹介します。
クエリパラメータを含む場合
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の機能が豊富で、想像以上に簡単にスタブサーバーを構築することができました!
外部サービスとの連携があるプロダクトを開発している方には非常におすすめなので、ぜひ使ってみてください。
最後に、バイセルではエンジニアを随時募集しております。興味のある方はぜひ以下の採用サイトをご覧ください。