スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

APIの開発に(なるべく)依存しないモバイルアプリ開発

Mobile Engineer の @chiiia12 です。 今回は試験的に私たちのチームで導入した "MockApiInterceptor" の取り組みについてご紹介します。 (今回は、Android アプリでの例を紹介します。)

背景

最初に私たちのチーム構成/開発の進め方について説明させてください。

私たちのサービスは、Web フロント・サーバーサイドを開発する "Web チーム" と Android/iOS アプリを開発する "Native アプリチーム" に分かれています。これまでの開発は Web チームの人数の多さもあり Web での機能開発が先行する形で行われ、Native アプリでの同じ機能開発は "後追い" する形で開発を進めてきたことが大半でした。したがって、既に API が用意されている状態でアプリの開発が開始できました。

今回、Native アプリの開発が先行する案件があり、一部の API が用意されていない状況で開発を進めなくてはいけませんでした。今までとは違う開発スタイルでスムーズに開発をすすめるために試行錯誤した取り組みの一部をご紹介します。

課題

API の開発が終わり開発環境へのデプロイを待たないとアプリの開発が進められないことに課題感を感じていました。

できるだけ Debug 用のコードを書かずに開発完了の状態に近づけるために、一部のエンドポイントのみ任意のレスポンスを返すツールを導入することにしました。

導入した MockApiInterceptor について

こちらの記事を参考に "MockApiInterceptor" というクラスを追加しました。OkHttp の Interceptor に追加し、設定したエンドポイントのみ Mock の Response を返すというシンプルなものです。

以下が Mock の JSON を返す Interceptor のクラスです。

class MockApiInterceptor : Interceptor {

  override fun intercept(chain: Interceptor.Chain): Response {
        if (BuildConfig.DEBUG) {
            val path = chain.request().url().uri().path
            MockEndpoint.from(path)?.apply {
                if (enabled) {
                    return mockResponse(chain, responseString)
                }
            }
        }
        return chain.proceed(chain.request())
    }

    private fun mockResponse(chain: Interceptor.Chain, response: String): Response {
        return Response.Builder()
            .request(chain.request())
            .message("")
            .code(200)
            .protocol(Protocol.HTTP_2)
            .body(
                ResponseBody.create(
                    MediaType.parse("application/json"),
                    response
                )
            )
            .build()
    }
}

enum class MockEndpoint(
    val enabled: Boolean,
    val path: String,
    val responseString: String
) {
    FETCH_USER(
        false,
        "/user",
        """
    {
    "user": {
      "login": "octocat",
      "id": 1,
      "avatar_url": "https://github.com/images/error/octocat_happy.gif",
    }
    }
    """
    ),
    FETCH_ISSUE(
        false,
        "/issues",
        """
           [{
    "id": 1,
    "title": "Found a bug",
    "body": "I'm having a problem with this.",
    "user": {
      "id": 1
    },
    "labels": [{
        "id": 208045946,
        "url": "https://api.github.com/repos/octocat/Hello-World/labels/bug",
        "name": "bug",
      }]
  }]
        """
    );

    companion object {
        fun from(path: String): MockEndpoint? =values().firstOrNull { it.path == path }
    }
}

この Interceptor を OkHttpClient の生成部分で追加します。

OkHttpClient.Builder().addInterceptor(MockApiInterceptor()).build()

この MockApiInterceptor を追加することで Debug 用の一時的なロジックを書く必要がなくなり、API がまだ用意されていなくても実装をすすめることができます。

また Response は定義した JSON を自由に編集すればよいので、Response によって特定の状態を作りたい時にも動作確認が便利になります。

注意したい Network Interceptor と Application Interceptor

今回のツールを導入するにあたり、Interceptor の仕組みについて勘違いしやすいポイントもあるので紹介します。

OkHttp の Interceptor には Application Interceptor と Network Interceptor の種類があるようです。ref: OkHttp/Interceptors

今回は実際に API にリクエストする前に Mock のレスポンスを返したいので 試行錯誤の結果 Application Intercetor として使用しました。

上で挙げた "MockApiInterceptor" のファイルを Network Interceptor として追加した場合、java.lang.IllegalStateException: network interceptor must call proceed() exactly once の Exception を throw します。Network Interceptor 内では、実際に HTTP Request を実行する chain.proceed() が1回呼ばれなければなりません。

Network Interceptor として chain.proceed() を使いつつ任意の Response を返したい時、404 となる Endpoint にアクセスすると IOException となり API 通信は失敗したものとして呼び出し側に扱われました。今回はまだ EndPoint の存在しない API に対して、仮の Response を返したいので chain.proceed() の呼び出しが必須である Network Interceptor ではなく Application Interceptor として追加することで解決することにしました。

今後の課題

今回はまだ開発が完了していない API について "MockApiInterceptor" を使いましたが、特定の状態のユーザーの挙動などを確認する際にも活用できることがわかり、より開発体験を向上することができると感じました。

今後必要に応じて、例えば以下のような機能も追加できれば動作確認にかかる時間を短縮できると考えています。

  • 状態変化によるリアルタイムな挙動を確認するために、アプリ起動中に Response を変えられる機能
  • より本番に近い環境で動作確認をするために、Response の一部の field のみ指定したものに変更できる機能

おわりに

API の Mock 用サーバーに使えるシステム、ライブラリは既に存在するかと思いますが、導入にはなんとなく腰が重いものでした。

今回のように、1つのクラスの追加と一行の設定を変更するだけで開発体験を改善することができるのは、コストパフォーマンスとしても良いと思っています。影響範囲も狭いので、不要になった時に捨てるのも簡単な点でも気軽に導入できました。参考になれば幸いです。

参考