49
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

OkHttp3 の MockWebServer を使う

Posted at

概要

HTTP Client のテストで役立つライブラリ MockWebServer について調べてみました。

MockWebServer とは

OkHttp のテストをするために作ったのだろうと思われるライブラリで、OkHttp のリポジトリ内にプロジェクトがあります。ライセンスは Apache License, Version 2.0 です。
HTTP 通信部分を Mock 化するのに使えます。今日日、 HTTP 通信を使わないアプリケーションもそうそうないでしょうから、需要は結構ありそうな感じがします。

OkHttp とは

Android 向けの便利なライブラリを大量に作っている Square, Inc. の開発した HTTP Client です。普通の Java ライブラリなので、Web アプリケーションや業務アプリケーションでも利用できます

Android 界隈では Apache の HTTP Client が Android OS 5.x から非推奨→削除となったことで、その代替ライブラリとして注目されています。

Square, Inc. とは

アメリカの決済サービス企業、日本では Android の便利なライブラリを作ってくれる企業として知られています。CEO は Twitter の共同創業者ジャック・ドーシー氏です。

Trend

Java テストツールのトレンド 2014/1~2016/5 で Google Trend のグラフを添付していたのがわかりやすかったので真似させてもらいます。いずれも2012年1月から2016年6月までのグラフです。

MockWebServer

GoogleTrend_MockWebServer.png

OkHttp

GoogleTrend_OkHttp.png

使い始める

build.gradle

依存を1つ追加するだけで使えます。なお、依存に OkHttp が含まれています。

build.gradle
dependencies {
......
    testCompile 'com.squareup.okhttp3:mockwebserver:3.2.0'
}

(任意) Eclipse Collections

下記のクラスを使うために Eclipse Collections を追加しました。必須ではないですが、あると便利です。

  1. Interval (Java SE8 の IntStream を便利にした感じのもの)
  2. Procedures#throwing (Lambda 式の中で Try-Catch を書かなくて済むので、ネストを深くしなくて済むという視覚的効果を期待)
build.gradle
dependencies {
......
    compile 'org.eclipse.collections:eclipse-collections:7.1.0'
}

公式のサンプルコードをいじる

GitHub repository の README.md に書かれていたサンプルコードを読んでも意味がまったくわからなかったので、自分でわかるように書き直してみました。

import java.io.IOException;

import org.eclipse.collections.impl.block.factory.Procedures;
import org.eclipse.collections.impl.list.Interval;
import org.junit.Test;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;

public class MockServerTest {

    @Test
    public void test() throws IOException, InterruptedException {
        // Create a MockWebServer. These are lean enough that you can create a new
        // instance for every unit test.
        final MockWebServer server = new MockWebServer();

        // Schedule some responses.
        server.enqueue(new MockResponse().setBody("hello, world!"));
        server.enqueue(new MockResponse().setBody("sup, bra?"));
        server.enqueue(new MockResponse().setBody("yo dog"));

        // Start the server.
        server.start();

        // Ask the server for its URL. You'll need this to make HTTP requests.
        final OkHttpClient client = new OkHttpClient();

        Interval.oneTo(3).each(Procedures.throwing(i -> {
            final Request  request  = new Request.Builder().url(server.url("/v1/chat/")).build();
            final Response response = client.newCall(request).execute();

            System.out.println(i);
            System.out.println(response.headers());
            System.out.println(response.body().string());
        }));

        // Shut down the server. Instances cannot be reused.
        server.shutdown();
    }
}
実行結果
6 11, 2016 6:44:23 午後 okhttp3.mockwebserver.MockWebServer$3 execute
情報: MockWebServer[64145] starting to accept connections
6 11, 2016 6:44:23 午後 okhttp3.mockwebserver.MockWebServer$4 processOneRequest
情報: MockWebServer[64145] received request: GET /v1/chat/ HTTP/1.1 and responded: HTTP/1.1 200 OK
1
Content-Length: 13
OkHttp-Sent-Millis: 1465638263689
OkHttp-Received-Millis: 1465638263695

hello, world!
6 11, 2016 6:44:23 午後 okhttp3.mockwebserver.MockWebServer$4 processOneRequest
情報: MockWebServer[64145] received request: GET /v1/chat/ HTTP/1.1 and responded: HTTP/1.1 200 OK
2
Content-Length: 9
OkHttp-Sent-Millis: 1465638263698
OkHttp-Received-Millis: 1465638263698

sup, bra?
6 11, 2016 6:44:23 午後 okhttp3.mockwebserver.MockWebServer$4 processOneRequest
情報: MockWebServer[64145] received request: GET /v1/chat/ HTTP/1.1 and responded: HTTP/1.1 200 OK
3
Content-Length: 6
OkHttp-Sent-Millis: 1465638263699
OkHttp-Received-Millis: 1465638263701

yo dog
6 11, 2016 6:44:23 午後 okhttp3.mockwebserver.MockWebServer$3 acceptConnections
情報: MockWebServer[64145] done accepting connections: socket closed

どうやらこれは単純な Mock エントリポイントを作って取得のテストをしているわけではない、と気付きました。


本当に最小限のことだけやるテスト

まだわかりにくいので、本当に Hello world レベルの UnitTest を勉強のために書いてみます。

本当に最小限のことだけやるテスト
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.io.IOException;

import org.junit.Test;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;

public class MockServerTest {

    @Test
    public void test() throws IOException, InterruptedException {
        final MockWebServer server = new MockWebServer();
        server.enqueue(new MockResponse().setBody("hello, world!"));
        server.start();

        final OkHttpClient client = new OkHttpClient();
        final Request  request  = new Request.Builder().url(server.url("/hello")).build();
        final Response response = client.newCall(request).execute();

        assertEquals("13", response.header("Content-Length"));
        assertNotNull(response.header("OkHttp-Sent-Millis"));
        assertNotNull(response.header("OkHttp-Received-Millis"));
        assertEquals("hello, world!", response.body().string());

        server.shutdown();
    }
}

順を追って説明します。

MockWebServer

Mock の Web サーバです。次に説明する MockResponse のキューを持っています。

MockResponse

Mock の レスポンスを定義するオブジェクトです。Body や Header や Status code を持たせることができます。

MockWebServer の用意

MockWebServer オブジェクトを初期化し、そのキューに Mock のレスポンスを追加していきます。今回はシンプルにテキストだけを Body に詰めます。キューを用意し終わったら MockWebServer オブジェクトの start メソッドを呼んでください。

final MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("hello, world!"));
server.start();

OkHttpClient で HTTP 通信する

下記はほぼ OkHttp の処理をするコードです。

final OkHttpClient client = new OkHttpClient();
final Request  request  = new Request.Builder().url(server.url("/hello")).build();
final Response response = client.newCall(request).execute();

MockWebServer オブジェクトの url メソッドを使い、引数で渡す任意のパスで仮 URL を発行します。このメソッドで得られるオブジェクトは HttpUrl という、OkHttp 固有のクラスですが、toString() すれば通常の URL 文字列を取得できます。HTTP Client はその仮 URL にアクセスします。

仮URLの発行
server.url("/hello") // http://localhost:64243/hello

ポート番号は指定しない限り、MockWebServer オブジェクト単位で変わっているようです。

Response の操作

レスポンスヘッダ

特に指定していない場合は下記3つが入っています。

  1. Content-Length
  2. OkHttp-Sent-Millis
  3. OkHttp-Received-Millis
Httpレスポンスヘッダの検査
assertEquals("13", response.header("Content-Length"));
assertNotNull(response.header("OkHttp-Sent-Millis"));
assertNotNull(response.header("OkHttp-Received-Millis"));

Body

キューに登録した通りの Body が入っています。

Bodyの検査
assertEquals("hello, world!", response.body().string());

終了

使い終わった MockWebServer オブジェクトは shutdown します。

server.shutdown();

MockResponse をいじる

自身を返すタイプの Setter を実装しているのでメソッドチェインでつなげられます。最近のライブラリはこういうのが増えたと感じます。

final MockResponse mock = new MockResponse()
                .setBody("hello, world!")
                .setHeader("Mock-Header",  "mock")
                .setHeader("Mock-HeaderB", "mock")
                .setHeader("Mock-HeaderB", "mocker")
                .setResponseCode(403)
                .setResponseCode(404);

setHeader

任意のヘッダを key - value ペアで設定します。複数回呼び出した場合は複数登録できます。 key が重複する場合は最後の値で上書きされます。上記の例だと key="Mock-HeaderB" の value は "mocker" になります。

setResponseCode

HTTP Status code を int で設定します。最後に指定されたものが有効となります。なお、3xx系や4xx系や5xx系を指定した場合はリクエスト失敗として扱われるようです。

setStatus

Status メッセージ(curl の --head オプションをつけると先頭に出てくる "HTTP/1.1 200 OK" のような文字列)を設定できます。ルールに則っていないメッセージを渡すと、

不正なメッセージの設定
.setStatus("Rotten Apple Error.")

HTTP リクエスト時に下記のような例外が発生します。

例外:java.net.ProtocolException
java.net.ProtocolException: Unexpected status line: Rotten Apple Error.
	at okhttp3.internal.http.StatusLine.parse(StatusLine.java:69)
……省略……

ルールに則ったメッセージであれば登録できますし、Status code も上書きされてしまいます。

正当なメッセージの設定
.setStatus("HTTP/1.1 666 Rotten Apple Error.")

実装を覗いたところ、 setResponseCode メソッドから呼び出されているようで、どう見ても内部的に利用するメソッドです。何で public で公開されているのかよくわかりません。

setResponseCode
public MockResponse setResponseCode(int code) {
    /* 省略 */
    return setStatus("HTTP/1.1 " + code + " " + reason);
}

Dispatcher を使う

GitHub repository の README.md サンプルで示されているのは MockResponse の queue を使う形式です。当然ながら queue は1回取り出したらなくなります。queue が空っぽの状態で MockWebServer にリクエストすると、レスポンスが返ってこないままタイムアウトまで停止してしまいます。

「何か違うんだよなあ」と思ったところ、README.md の下の方に Dispatcher というのが紹介されていました。

Dispatcher を使うコード全体

import java.io.IOException;

import org.eclipse.collections.impl.block.factory.Procedures;
import org.eclipse.collections.impl.factory.Lists;
import org.junit.Test;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;

public class MockServerTest {

    @Test
    public void test() throws IOException, InterruptedException {
        final MockWebServer server = new MockWebServer();

        final Dispatcher dispatcher = new Dispatcher() {

            @Override
            public MockResponse dispatch(final RecordedRequest request) throws InterruptedException {
                if (request == null || request.getPath() == null) {
                    return new MockResponse().setResponseCode(400);
                }
                switch (request.getPath()) {
                    case "/hello":
                        return new MockResponse().setBody("Hello world!").setResponseCode(200);
                    case "/transferred":
                        return new MockResponse().setResponseCode(301);
                    case "/forbidden":
                        return new MockResponse().setResponseCode(403);
                    default:
                        return new MockResponse().setResponseCode(404);
                }
            }
        };

        server.setDispatcher(dispatcher);

        server.start();

        final OkHttpClient client = new OkHttpClient();

        ArrayAdapter.adapt("/hello", "/transferred", "/forbidden", "/notexists", "/hello")
            .each(
                Procedures.throwing(path -> {
                    final HttpUrl url = server.url(path);
                    final Request request = new Request.Builder().url(url).build();
                    final Response response = client.newCall(request).execute();

                    System.out.println(url.toString());
                    System.out.println(response.message());
                    System.out.println(response.isSuccessful());
                    System.out.println(response.code());
                    System.out.println(response.headers());
                })
            );

        server.shutdown();
    }
}

実行結果

このように、何度同じ URL にアクセスしても同じ body を取得でき、存在しないパスにはアクセスできない MockWebServer が出来上がります。

実行結果
6 11, 2016 8:21:14 午後 okhttp3.mockwebserver.MockWebServer$3 execute
情報: MockWebServer[58567] starting to accept connections
6 11, 2016 8:21:15 午後 okhttp3.mockwebserver.MockWebServer$4 processOneRequest
情報: MockWebServer[58567] received request: GET /hello HTTP/1.1 and responded: HTTP/1.1 200 OK
http://localhost:58567/hello
OK
true
200
Content-Length: 12
OkHttp-Sent-Millis: 1465644075148
OkHttp-Received-Millis: 1465644075154

http://localhost:58567/transferred
Redirection
false
301
Content-Length: 0
OkHttp-Sent-Millis: 1465644075158
OkHttp-Received-Millis: 1465644075159

6 11, 2016 8:21:15 午後 okhttp3.mockwebserver.MockWebServer$4 processOneRequest
情報: MockWebServer[58567] received request: GET /transferred HTTP/1.1 and responded: HTTP/1.1 301 Redirection
http://localhost:58567/forbidden
Client Error
false
403
Content-Length: 0
OkHttp-Sent-Millis: 1465644075161
OkHttp-Received-Millis: 1465644075163

6 11, 2016 8:21:15 午後 okhttp3.mockwebserver.MockWebServer$4 processOneRequest
情報: MockWebServer[58567] received request: GET /forbidden HTTP/1.1 and responded: HTTP/1.1 403 Client Error
http://localhost:58567/notexists
Client Error
false
404
Content-Length: 0
OkHttp-Sent-Millis: 1465644075163
OkHttp-Received-Millis: 1465644075164

6 11, 2016 8:21:15 午後 okhttp3.mockwebserver.MockWebServer$4 processOneRequest
情報: MockWebServer[58567] received request: GET /notexists HTTP/1.1 and responded: HTTP/1.1 404 Client Error
http://localhost:58567/hello
OK
true
200
Content-Length: 12
OkHttp-Sent-Millis: 1465644075165
OkHttp-Received-Millis: 1465644075165

6 11, 2016 8:21:15 午後 okhttp3.mockwebserver.MockWebServer$4 processOneRequest
情報: MockWebServer[58567] received request: GET /hello HTTP/1.1 and responded: HTTP/1.1 200 OK
6 11, 2016 8:21:15 午後 okhttp3.mockwebserver.MockWebServer$3 acceptConnections
情報: MockWebServer[58567] done accepting connections: socket closed

以下、詳細を見ていきます。

import

OkHttp 本体にも同名のクラスがあるので注意してください。

import okhttp3.mockwebserver.Dispatcher;

Dispatcher の定義

指定された Path に対し MockResponse を振り分けることができます。

Dispatcher は abstract クラスであり、dispatch メソッドを実装する必要があります。dispatch メソッドは RecordedRequest オブジェクトの request を引数に取り、request が持つ Path を見て、返す MockResponse を分けます。Queue ではないので、何度同じ Path にアクセスが来ても同様の MockResponse を返します。

Dispatcher
final Dispatcher dispatcher = new Dispatcher() {

    @Override
    public MockResponse dispatch(final RecordedRequest request) throws InterruptedException {
        if (request == null || request.getPath() == null) {
            return new MockResponse().setResponseCode(400);
        }
        switch (request.getPath()) {
            case "/hello":
                return new MockResponse().setBody("Hello world!").setResponseCode(200);
            case "/transferred":
                return new MockResponse().setResponseCode(301);
            case "/forbidden":
                return new MockResponse().setResponseCode(403);
            default:
                return new MockResponse().setResponseCode(404);
        }
    }
};

Java SE7 以降であれば String に対し switch-case が使えます。が、switch で評価する式( ()の中身)に null が入ってしまうと NullPointerException が発生しますので、null チェックを書かないといけません。

Dispatcher の設定

MockWebServer オブジェクトに先ほどの Dispatcher を設定します。

server.setDispatcher(dispatcher);

リクエスト

下記の Path に対し、順に HTTP リクエストを実施します。

  1. /hello
  2. /transferred
  3. /forbidden
  4. /notexists
  5. /hello
ArrayAdapter.adapt("/hello", "/transferred", "/forbidden", "/notexists", "/hello")
    .each(
        Procedures.throwing(path -> {
            final HttpUrl  url      = server.url(path);
            final Request  request  = new Request.Builder().url(url).build();
            final Response response = client.newCall(request).execute();

            System.out.println(url.toString());
            System.out.println(response.message());
            System.out.println(response.isSuccessful());
            System.out.println(response.code());
            System.out.println(response.headers());
        })
    );

Dispatcher で振り分けの設定をしていない Path である /notexists は 404 となります。
また、/hello は2回呼び出しても同じ Body が取得できます


まとめ

MockWebServer は HTTP クライアントや HTTP 接続部分のユニットテストを書く際に役立つライブラリです。通常の利用では Dispatcher を定義して、それを MockWebServer オブジェクトにセットして起動し、 HTTP 接続のテストコードを書くとよいでしょう。


参考

  1. MockWebServer(GitHub repository)
  2. OkHttpのMockWebServerをJUnitで利用する
  3. HTTPステータスコード
49
41
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
49
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?