WireMock が便利だった
WireMock を導入する機会があって便利だったのでメモ。
余談ですが、昨日書いた mermaid に対応してみた この記事は今日の記事で図を書きたいがために対応したのであった。
実現したかったこと
flowchart RL
subgraph テスト対象
client
api
end
ex(External Service)
db[(Database)]
mock(wiremock)
client <--> api
api <--> mock
mock <-->|Request Not Mached| ex
api <-.->|before| ex
ex <--> db
上図のように api
から External Service
へのリクエストがある場合を考える。
api
, client
は結合した上で様々なバリエーションのデータをテストしたいとなった時には、 Database
の内容を書き換えることが簡単にできるのであればそれが最も実際の挙動に近いテストができるのでそれが可能な場合はそうすべきと考える。
が今回の場合には、 External Service
や Database
を書き換えることが難しい (が External Serivce
自体はほぼ APIゲートウェイ としての役割しかない) という制約があったため、 WireMock を api -> External Serive
の間に立てることで特定のリクエストの場合には固定のレスポンスを返すようにしつつ、それ以外のリクエストについては通常通り External Serivce
にリクエストさせるを実現したい。
またコンテナオーケストレーションとして k8s を利用していることとします。
実現方法
Wiremock をどう起動するか
ところで WireMock には https://wiremock.org/docs/ にもあるようにいくつかの起動方法が存在します。
今回は、他のアプリケーションも全てコンテナで動いているし、コンテナオーケストレーションに k8s を利用していることもあり Running in Docker を選択しています。
また Running as as Standlone Process にあるような環境変数で例えばポートなどを変更できるため以下のような Dockerfile
を作ってイメージをビルドするようにしています。
# Dockerfile
FROM wiremock/wiremock:2.35.0
COPY mappings /home/wiremock/mappings # -> 後述します
CMD ["--port", "8444"]
WireMock は多くの機能を有しているのですが今回は特に2つの機能を主に利用しました * Stubbing -> あるリクエストパターンにマッチした際に固定のレスポンスを返す機能 * Proxying -> リクエストを別のドメインにフォワーディングする機能
それぞれの機能は、以下のような json を /mappings
配下に配置することで簡単に実現することができます。
Stubbing の例 (以下のような json を stub させたい数だけ配置する)
matchesJsonPath
の部分がかなり癖があるが、
Request Matching にもある通り JSON Path に依存しているっぽい (?) のでこの辺りは手探りで書く必要があってやや面倒ではあった。
{
"request": {
"method": "POST",
"url": "/some/url",
"bodyPatterns" : [
{
"matchesJsonPath" : {
"expression": "$.id",
"equalTo": "xxxxxx"
}
}
]
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"message": "Hello World"
}
}
}
Proxying の例 (Stubbing にマッチしなかったリクエストを全て本来のドメインにリクエストにしたいので priority を設定している)
Stubbing に以下のように記載されている。 > When unspecified, stubs default to a priority of 5^ where 1 is the highest priority and Java Integer.MAX_VALUE (i.e., 2147483647) is the minimum priority.
{
"priority": 999,
"request": {
"method": "ANY",
"urlPattern": ".*"
},
"response": {
"proxyBaseUrl": "http://otherhost.com"
}
}
というわけで Dockerfile
を再掲
# Dockerfile
FROM wiremock/wiremock:2.35.0
COPY mappings /home/wiremock/mappings # -> ローカルで定義して json をコピーして配置する
CMD ["--port", "8444"] # -> 別のポートを指定して起動する
Pod の定義
WireMock をコンテナとして起動する方法の概要は紹介したので次はコンテナオーケストレーションとしてどういう設定になるかを紹介します。 繰り返しになりますが、 k8s を使っているものとします。
ざっくりは、以下のような構成に最終的になりました。
(※ 正確には、LB などがあって通信をルーティングしているし、そもそもクラスタの構成も反映すべきですがあくまでもコンセプトが伝わればという意図で簡易的な図にしてます。)
flowchart RL
subgraph Frontend Pod
client
end
subgraph API Pod
api
mock(wiremock)
end
subgraph External Pod
ex(External Service)
db[(Database)]
end
client <--> api
api <-->|localhost 通信| mock
mock <--> ex
ex <--> db
Kubernetesクラスター内のPodは、主に次の2種類の方法で使われます。
単一のコンテナを稼働させるPod。「1Pod1コンテナ」構成のモデルは、Kubernetesでは最も一般的なユースケースです。このケースでは、ユーザーはPodを単一のコンテナのラッパーとして考えることができます。Kubernetesはコンテナを直接管理するのではなく、Podを管理します。
協調して稼働させる必要がある複数のコンテナを稼働させるPod。単一のPodは、密に結合してリソースを共有する必要があるような、同じ場所で稼働する複数のコンテナからなるアプリケーションをカプセル化することもできます。これらの同じ場所で稼働するコンテナ群は、単一のまとまりのあるサービスのユニットを構成します。たとえば、1つのコンテナが共有ボリュームからファイルをパブリックに配信し、別のサイドカーコンテナがそれらのファイルを更新するという構成が考えられます。Podはこれらの複数のコンテナ、ストレージリソース、一時的なネットワークIDなどを、単一のユニットとしてまとめます。
Pod にもあるように k8s の一般的なユースケースは、上記のように 「1Pod1コンテナ」構成のモデルになると思いますが、今回は2つ目のユースケースのように API Pod
のプロキシとしてサイドカーを立てるような方針を取ってます。
Pod#Podネットワーク にあるように同一Pod内の通信は、ローカルホストでのポート指定の通信となることに注意してください。
k8s の deployments では以下のように指定します
# k8s の dployments
containers:
- name: api
image: "DOCKER_IMAGE_REPO/api:latest"
ports:
- name: http
containerPort: 8083
protocol: TCP
- name: wiremock
image: "DOCKER_IMAGE_REPO/wiremock:latest"
- name: http
containerPort: 8444
protocol: TCP
そしてアプリケーションのレイヤーで API Pod
から Wiremock
への通信を localhost:8444
で行うようにしています。