Mock API ライブラリ WireMock を使ってみた

f:id:sugimomoto:20211217224122p:plain

こんにちは。最近またダイエットに励んでいる杉本です。

この記事は Web API Advent Calendar 2021 17日目です。

qiita.com

というわけで Withings というブランドの体重計をこの機会に買ってしまいました。

www.withings.com

この体重計の一つのポイントがAPIの存在です。

developer.withings.com

そこで、いそいそとJavaでAPI連携のプログラムを趣味で書いているのですが、API連携レイヤーを担うライブラリを書くにあたって、やっぱり面倒なのがAPIの実際のリクエストを検証するテストですね。

色々と調べていたらJavaのJUnitに組み込むならWireMock というのが便利な感じだったので、ちょっと検証した結果をまとめたいと思います。

WireMock.org

WireMock のいいところ

以下のようなリクエストの定義ファイルを作るだけで、テスト用のMock API Server が組み込める感じです。

{
    "mappings": [
        {
            "request": {
                "method": "GET",
                "urlPath": "/foo",
                "headers": {
                    "Accept": {
                        "equalTo": "application/json"
                    }
                }
            },
            "response": {
                "status": 200,
                "body": "{\"bar\":\"buzz\"}",
                "headers": {
                    "Content-Type": "application/json"
                }
            }
        }
    ]
}

さらにナイスなのはリクエストが間違っていた時にいかのような差分を返してくれるところですね!

ただのMock APIだと、リクエストの失敗だけ、みたいな感じになりがちですが、これだとかなりプログラムの修正が捗る感じです。

                                               Request was not matched
                                               =======================
-----------------------------------------------------------------------------------------------------------------------
| Closest stub                                             | Request                                                  |
-----------------------------------------------------------------------------------------------------------------------
                                                           |
GET                                                        | GET
[path] /foo                                                | /foo
                                                           |
Accept: application/json                                   | Accept: application/jso                             <<<<< Header does not match
                                                           |
                                                           |
-----------------------------------------------------------------------------------------------------------------------

セットアップ

WireMockですが、スタンドアローンで利用するパターンとJUnit等Java に組み込んで利用するパターンの2種類の利用方法があります。

スタンドアローンはWireMock自身がホスティングしているWebサービスのMocklab バージョンか、シンプルにJarを実行するもの、Docker などにも配置できるイメージなどでも提供されています。

get.mocklab.io

WireMock.org

WireMock.org

スタンドアローンはマイクロサービスなどのテストで利用するのも良いですね。

ただ、今回はシンプルなライブラリのテストで利用したいので JUnit ベースで利用してみました。

Mavenリポジトリは以下から。

https://mvnrepository.com/artifact/com.github.tomakehurst/WireMockmvnrepository.com

使い方

JUnitで利用する場合、まずJUnitのテストクラスにWireMockRuleのインスタンスを追加します。

WireMock.org

第一引数で指定しているのはPort Numberですね。

import com.github.tomakehurst.wiremock.junit.WireMockRule;

/**
 * Unit test for simple App.
 */
public class AppTest 
{
    @Rule
    public WireMockRule wireMockRule = new WireMockRule(8081);

そして必要になるのが、APIリクエストを検証するための設定ファイルです。

「\src\test\resources\mappings\my-mapping.json」という形でJSONファイルで定義します。デフォルトではこのパスに配置されたものが参照されるみたいですね。

WireMock.org

例えばOkHTTPを使って「http://localhost:8081/foo」にGETリクエストを送り、「{"bar":"buzz"}」というJSONのレスポンスを取得するテストを書いてみましょう。

import okhttp3.*;

/* 省略 */

    private static final OkHttpClient client = new OkHttpClient();

    @Test
    public void mockServerTest() throws IOException{
        Request request = new Request.Builder().url(url + "foo").header("Accept", "application/json").get().build();

        Response response = client.newCall(request).execute();
        assertEquals("{\"bar\":\"buzz\"}",response.body().string());
    }

「my-mapping.json」のサンプルは以下のような感じです。

{
    "mappings": [
        {
            "request": {
                "method": "GET",
                "urlPath": "/foo",
                "headers": {
                    "Accept": {
                        "equalTo": "application/json"
                    }
                }
            },
            "response": {
                "status": 200,
                "body": "{\"bar\":\"buzz\"}",
                "headers": {
                    "Content-Type": "application/json"
                }
            }
        }
    ]
}

ポイントはリクエストのマッチングパターンでしょう。

Acceptの検証を行っている箇所は、現在「equalTo」で完全一致ですが、例えば以下のような部分一致の形にもできますし

      "Accept" : {
        "contains" : "json"
      }

URLも正規表現でマッチングをかけることができます。例えばURLパスにリソースのIDなどが動的に入る場合は、このアプローチが活用できますね。

{
  "request": {
    "urlPattern": "/your/([a-z]*)\\?and=query"
    ...
  },
  ...
}

Query Parameters

APIリクエストでチェックが面倒なものと言えば、Query Parametersではないでしょうか。

固定で構成しているならまだしも、動的に組み立てる側面が強いプログラムであれば、柔軟なチェック方法が欲しいですよね。

@Test
public void queryParametersTest() throws IOException{
    
    Request request = new Request.Builder().url(url + "foo_query_parameters?$top=10&$select=id,name").header("Accept", "application/json").get().build();

    Response response = client.newCall(request).execute();
    assertEquals("{\"bar\":\"buzz\"}",response.body().string());
}

WireMockでは以下の「queryParameters」でそれぞれのパラメータを指定して、検証することができるので、便利です。もちろん、equalToの部分は「contains」や「matches」を使ってもいいですね。

{
    "method": "GET",
    "urlPath": "/foo_query_parameters",
    "queryParameters": {
        "$select": {
            "equalTo": "id,name"
        },
        "$top": {
            "equalTo": "10"
        }
    },
    "headers": {
        "Accept": {
            "equalTo": "application/json"
        }
    }
}

Request Body

Request Body にJSONを使うパターンも多いかと思います。

@Test
public void postJsonRequestTest() throws IOException{

    List users = new ArrayList();
    users.add(new User(1,"kazuya"));
    users.add(new User(2,"hitomi"));

    String postJson = new ObjectMapper().writeValueAsString(users);

    RequestBody body = RequestBody.create(postJson,MediaType.parse("application/json"));
    Request request = new Request
        .Builder()
        .url(url + "foo_json_user_post")
        .header("Accept", "application/json")
        .header("Content-Type", "application/json")
        .post(body).build();

    Response response = client.newCall(request).execute();
    assertEquals(200, response.code());
}

以下のように「bodyPatterns」を使って、JSONを表現し、マッチングがかけられます。

ちなみに「"ignoreArrayOrder" : true」をつけると配列の順番が変わってもOKと細かな点が優しくできます。

{
    "method": "POST",
    "urlPattern": "/foo_json_user_post",
    "headers": {
        "Content-Type": {
            "contains": "application/json"
        }
    },
    "bodyPatterns": [
        {
            "equalToJson": [
                {
                    "id": 2,
                    "name": "hitomi"
                },
                {
                    "id": 1,
                    "name": "kazuya"
                }
            ],
            "ignoreArrayOrder" : true
        }
    ]
}

Authentication

認証周りはCookieやOAuth TokenだとそのままHeaderをチェックすればいいだけと言えばだけですが、例えばBasic認証だと専用のアプローチが存在していたりします。

@Test
public void basicAuthenticationTest() throws IOException{
    
    Request request = new Request.Builder()
        .url(url + "foo_basic_authentication")
        .header("Accept", "application/json")
        .header("Authorization", Credentials.basic("userName", "password"))
        .get().build();

    Response response = client.newCall(request).execute();
    assertEquals("{\"bar\":\"buzz\"}",response.body().string());
}

以下の「basicAuthCredentials」で検証できます。もちろんHeaderにBase64エンコードしたものを入れておいて、検証しても問題ないですが、こういう専用のものがあると、いちいち変換の手間が無いので良いですね。

{
    "method": "GET",
    "urlPath": "/foo_basic_authentication",
    "headers": {
        "Accept": {
            "equalTo": "application/json"
        }
    },
    "basicAuthCredentials": {
        "username": "userName",
        "password": "password"
    }
}

おわりに

というわけで、CData Blog ですが CData製品はあまり関係無いネタでした。

ただ、Mock APIを作りたくなるシチュエーションは多いので、スタンドアローンとして使って、CData Driverを作るときにも活用できるなーと思ったりでした。

ちなみに、以下の記事でCData製品内部のテストアプローチだったりも語っていますので、よかったら参考にしてみてください。

www.cdatablog.jp

トライアル・お問い合わせ

関連コンテンツ