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 ServiceDatabase を書き換えることが難しい (が External Serivce 自体はほぼ APIゲートウェイ としての役割しかない) という制約があったため、 WireMockapi -> 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 で行うようにしています。