【Kotlin】 Mock Web Server を使ったAPIユニットテストの書き方


Overview

検索ウインドウに検索キーワードを入力し、検索ボタンを押すと、API経由で画像を表示する簡単なAndroidアプリケーションを作成しました。

設計は次の図の通りです。

  • Viewレイヤーでは、キーワードを受け取り、ボタンのイベントが発火したら、Presenterに通知します。

  • Presenterレイヤーでは、Viewから渡されたキーワードをパラメーターに、ServiceレイヤーのAPIメソッドを呼び出します。

  • Serviceレイヤーでは、外部のAPI(https://api.unsplash.com)から画像のURIを取得します。

  • APIの結果に応じて、Presenterが持っているデータソースを更新します。

  • Presenterは、データソースの更新をViewに通知し、Viewをアップデートさせます。

Serviceレイヤーのユニットテストに、okhttp3のMockWebServerを使いました。

その際にちょっと詰まったので、使い方を残します。

必要な設定

1. ライブラリの導入

これらをモジュールのbuild.gradleに追加します。

androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.0.0'
androidTestImplementation 'com.android.support.test:runner:1.0.2'

2. 設定

2.1 ネットワークパーミッションの追加

AndroidManifest.xmlに、ネットワークのパーミッションを追加します。

<uses-permission android:name="android.permission.INTERNET" />

2.2 localhostとの通信を許可

localhostとの通信を許可するために、AndroidManifest.xmlのapplicationに次の設定を追加します。

<application
  ...
  android:usesCleartextTraffic="true"
  ...>

(あくまで、開発環境での設定です。)

(option) 2.3 testInstrumentRunnerの設定

モジュールのbuild.gradle内の testInstrumentationRunner
"android.support.test.runner.AndroidJUnitRunner"になっている場合は、"androidx.test.runner.AndroidJUnitRunner"に変更します。

これは、サポートするテストフレームワークに依存します。詳細はこちら

ここまでできたら、一度リビルドします。

3. テストファイルの作成

テスト対象となるクラス(今回はserviceクラス)をcmd+shift+tで、テストファイルを作ります。

今回はandroidのJUnitRUnnerを使うのでandroidTest以下に追加します。

4. テストの作成

mock web server を使うには、大きく4つのステップがあります。

  1. mockサーバー立てる
  2. レスポンスのインスタンスを作成する
  3. mockサーバーにインスタンスをenqueueする
  4. エンドポイントを叩く

順に解説します。

4.1 mockサーバーのインスタンスを作成する

テストクラスのコンストラクタ内で、モックサーバーをインスタンスを生成します。

private val webServer = MockWebServer()

@Beforeなどで、モックサーバーをlisten状態にします。

webServer.start(8080)

これで、localhost:8080で、モックサーバーが待ち受けている状態になります。

4.2 レスポンスのインスタンスを作成する。

MockResponse()で、レスポンスクラスを生成します。

private val successResponse = MockResponse().apply {
  setResponseCode(200)
  setHeader("Content-Type", "application/json")
  setHeader("Server", "Cowboy")
  setBody(sampleSuccessResponseData)
}

private val failResponse = MockResponse().apply {
  setResponseCode(401)
  setHeader("Content-Type", "application/json")
  setBody(sampleFailedResponseData)
}

private val sampleSuccessResponseData = """
{
  "hoge": "fuga"
}
""".trimIndent()

private val sampleFailedResponseData = """
{
  "err": "unauthorized"
}
""".trimIndent()

setResponseCodeで、レスポンスコードを設定できます。

setHeaderで、ヘッダーを設定できます。

setBodyで、レスポンスボディを設定できます。

4.3 mockサーバーにレスポンスをenqueueする

webServer.enqueue(successResponse)
webServer.enqueue(failResponse)

アクセスの度に、1つずつレスポンスがdequeueされます。

4.4 エンドポイントを叩く

Serviceクラスに、エンドポイントをセットします。

私の場合、RetroFit2.0を使っていたので、次のようにしました。

object UnsplashService {

    var END_POINT = "https://api.unsplash.com/"

    fun getImageURL(completion: <コールバック処理>, query: String) {
        Retrofit
            .Builder()
            .baseUrl(END_POINT)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(UnsplashAPI::class.java)
            .getImage(query)
            .enqueue(<コールバック処理>)
    }

    fun setEndPoint(url: String) {
        END_POINT = url
    }
}

setEndPointメソッドを生やしておいて、testするときは、

UnsplashService.setEndPoint("http://localhost:8080")

として、モックサーバーを叩くように変更しました。

また、テスト処理の後にはサーバーを落とすようにした方が、お行儀がいいみたいです。

@After
fun cleanUp() {
  webServer.shutdown()
}

補足

モックサーバーに送られてきたリクエストが正しいかどうかをテストしたい場合は、以下のようにしてリクエストを取り出すことができます。

webServer.takeRequest().headers[<ヘッダー名>]