おすすめモックサーバーの紹介(MockServer,WireMock)

モックサーバーとは

プログラムで外部サービスにRESTアクセスし、結果を使用することはよくあります。
問題はテストで、開発環境においては外部サービス自体をモック化して任意のレスポンスを返したいことも多いと思います。

今回そういったケースが必要になったため都元氏に相談したところ、
↓みたいなのがあるよーと教えてもらったので、それぞれ特徴を確認してみたいと思います。

本記事ではそういった場合に使用するモックサーバーについて紹介します。

環境

今回使用した動作環境は以下のとおりです。

  • OS : MacOS X 10.12.4
  • Java : 1.8.0_121-b13
  • Gradle : 3.5

モックサーバーライブラリ2選

MockServer

MockServerは、モックサーバツールでもメジャーなものの1つです。

・MockServerの特徴

  • 期待したリクエストがきたときに任意のモックレスポンスを返す
  • 期待したリクエストがきた際、対象のリクエストをフォワードする
  • リクエストが期待どうりだった場合、コールバックを実行してレスポンスを生成する

・MockServerのプロキシ機能
MockServerの特徴であるプロキシ機能、下記のような機能をもっています。

  • ポート転送
  • Webプロキシ(HTTPプロキシ)
  • HTTPSトンネリングプロキシ
  • SOCKSプロキシ処理(dynamic port foward)
  • リクエスト/レスポンスのRecord機能

・MockServerを使ってみる
ではMockServerを使ってみましょう。
まずはbuild.gradleにMockServerとApache HttpComponentsの依存関係を記述します。

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    compile group: 'org.mock-server', name: 'mockserver-netty', version: '3.10.7'
    compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.3'
}

次に、MockServerを使ってテストを作成します。
MockServerRuleを使うと、テストが実行される前にMockServerを起動、テスト完了後にMockServerを停止します。
今回はモックサーバを1080番ポートで起動します。

    @Rule
    public MockServerRule mockServerRule = new MockServerRule(this, 1080);

    private MockServerClient mockServerClient;

ちなみに、ポート番号を指定しなければランダムに選択した空きポートで起動してくれます。

mockServerClientを使用し、期待するリクエストとそれに対応するレスポンスを定義します。

        //setup
        mockServerClient
                .when(
                        HttpRequest.request()
                                .withMethod("GET")
                                .withPath("/foo")
                )
                .respond(
                        HttpResponse.response()
                                .withStatusCode(200)
                                .withHeader(new Header("Content-Type", "application/json; charset=utf-8"))
                                .withBody("{\"bar\":\"buzz\"}")
                );

ここでは、GETメソッドで/fooに対してリクエストをした場合、ステータスコード200でwithBodyにあるレスポンスを返します。

HttpClientを使って、上で定義したモックサーバにリクエストを発行します。
レスポンスを検証してみると、定義したとおりの結果がかえってくることがわかります。

        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://localhost:1080/foo");

        //execute
        org.apache.http.HttpResponse response = httpclient.execute(httpGet);

        //assert
        assertThat(EntityUtils.toString(
            response.getEntity(), StandardCharsets.UTF_8), is("{\"bar\":\"buzz\"}"));

MockServerのテストコード全文です。

import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.Rule;
import org.junit.Test;
import org.mockserver.client.server.MockServerClient;
import org.mockserver.junit.MockServerRule;
import org.mockserver.model.HttpRequest;
import org.mockserver.model.HttpResponse;

import java.nio.charset.StandardCharsets;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class MockServerTest {
    @Rule
    public MockServerRule mockServerRule = new MockServerRule(this, 1080);

    private MockServerClient mockServerClient;

    @Test
    public void test() throws Exception {

        System.out.println("using port : " + mockServerRule.getPort());

        //setup
        mockServerClient
                .when(
                        HttpRequest.request()
                                .withMethod("GET")
                                .withPath("/foo")
                )
                .respond(
                        HttpResponse.response()
                                .withStatusCode(200)
                                .withHeader(new Header("Content-Type", "application/json; charset=utf-8"))
                                .withBody("{\"bar\":\"buzz\"}")
                );

        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://localhost:1080/foo");

        //execute
        org.apache.http.HttpResponse response = httpclient.execute(httpGet);

        //assert
        assertThat(EntityUtils.toString(response.getEntity(), 
        StandardCharsets.UTF_8), is("{\"bar\":\"buzz\"}"));
    }

}

WireMock

WireMockもメジャーなモックサーバーツールです。
googleトレンドをみると、こっちのほうが人気かも?  

APIモック/サービス仮想化のツールとよばれているみたいです。
Androidのテストではこれがよく選択されている様子。

WireMockの特徴
基本的なモックサーバとしての機能に加え、下記のような特徴をもっています。

  • リクエスト/レスポンスのRecord機能
  • ステートフル動作のシミュレーション
  • スタブ設定をjsonファイルで可能

スタブをjsonファイルで設定可能
個人的にはこの機能が気に入りました。
これはモックサーバのリクエスト/レスポンスをjsonファイルに定義しておき、サーバ起動時に読み込みます。
予め定義しておくことで、テストコードが簡潔に記述できます。
(MockServerでもできるかとおもったけど方法がみつからず)  

WireMockを使ってみる
WireMockでもテストを実行してみます。 build.gradleにWireMockライブラリを追加しましょう。

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
    testCompile "com.github.tomakehurst:wiremock:2.6.0"
    compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.3'
}

WireMockRuleを使ってテストを作成します。
WireMockRuleもMockServerRuleを同じように、テストが実行される前にサーバを起動、テスト完了後にサーバを停止します。
引数にはポート番号を渡します。何も指定しなければ8080ポートでモックサーバが起動します。

    @Rule
    public WireMockRule wireMockRule = new WireMockRule(8080);

次に、期待するリクエストとレスポンスを定義します。
これはjsonファイルとして定義し、src/test/resources/mappings以下に配置します。
※複数配置可能

このjsonファイルはモックサーバ起動時に自動的にロードされます。
[src/test/resources/mappings/my-mapping.json]

{
  "request": {
    "method": "GET",
    "urlPattern": "/foo",
    "headers": {
      "Accept": {
        "matches": "text/.*"
      }
    }
  },
  "response": {
    "status": 200,
    "body": "{\"bar\":\"buzz\"}",
    "headers": {
      "Content-Type": "application/json"
    }
  }
}

MockServerの例とおなじようにリクエストとレスポンスを定義しています。

WireMockではAdmin REST API(/__admin)にアクセスすることで、マッピング情報や設定の確認/更新を行うことができます。
参考:Admin API Reference

        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://localhost:8080/__admin");
        org.apache.http.HttpResponse response = httpclient.execute(httpGet);
        System.out.println(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8));

jsonファイルで定義したモックサーバに対してリクエストを実行とレスポンスの検証をします。

        httpGet = new HttpGet("http://localhost:8080/foo");
        httpGet.setHeader(new BasicHeader("Accept","text/plain"));
        response = httpclient.execute(httpGet);

        //assert
        assertThat(EntityUtils.toString(response.getEntity(), 
        StandardCharsets.UTF_8), is("{\"bar\":\"buzz\"}"));

WireMockテストの全文です。

import com.github.tomakehurst.wiremock.junit.WireMockRule;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.junit.Rule;
import org.junit.Test;

import java.nio.charset.StandardCharsets;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

public class WireMockTest {
    @Rule
    public WireMockRule wireMockRule = new WireMockRule(8080);

    @Test
    public void test() throws Exception {

        //check mappings
        CloseableHttpClient httpclient = HttpClients.createDefault();
        HttpGet httpGet = new HttpGet("http://localhost:8080/__admin");
        org.apache.http.HttpResponse response = httpclient.execute(httpGet);
        System.out.println(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8));

        //execute
        httpGet = new HttpGet("http://localhost:8080/foo");
        httpGet.setHeader(new BasicHeader("Accept","text/plain"));
        response = httpclient.execute(httpGet);

        //assert
        assertThat(EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8), is("{\"bar\":\"buzz\"}"));
    }

}

まとめ

今回は2つのモックサーバツールについて調べてみました。
どちらも活発に開発されており、使いやすいツールだと思いますが、
jsonファイルでリクエスト/レスポンスの設定ができるのが良さそうなので、自分的にはWireMockかなーと思っています。

参考サイトなど