【Android】MockWebServerを使用してAPI通信部分のUnit Testを作成する

## はじめに
Androidアプリ開発では、API通信部分にRetrofitを採用するケースが多いと思います。
API通信部分のUnit Testを作成しようと思った時に、MockWebServerを使うと、ネットワーク通信を伴わない形でテストを作成できるとのこと。
今回はMockWebServerを使ったAPI通信部分のUnit Testを作成する方法をまとめていきたいと思います。

## MockWebServerとは
[Retrofit](https://square.github.io/retrofit/)と [OkHttp](https://square.github.io/okhttp/)でお馴染みのSquare社によって開発されたライブラリです。
実際にはOkHttpの一部という位置付けのものになるみたいです。（MockWebServerのREADMERは[こちら](https://github.com/square/okhttp/tree/master/mockwebserver)）

OkHttpのAPI通信部分のテストを作る際にモックとして使うもので、リクエストに対する任意のレスポンスを設定でき、ネットワーク通信を伴わずにローカル環境でテストを実施することができます。

## 環境準備
MockWebServerの導入の前に、諸々の環境を整えていきたいと思います。
今回はAPI通信先として[GitHub API](https://docs.github.com/ja/rest?apiVersion=2022-11-28)を設定し、ユーザー名を指定してリポジトリ一覧を取得するようなAPI通信処理を用意します。

### 依存関係の追加
API通信の部分は Retrofitとmoshiを使用していきたいと思いますので、関係するライブラリの依存関係を追加してきます。
``` build.gradle.kt(app)
dependencies {
    // 記事執筆時点の最新バージョンを指定
    // Retrofit
    val retrofitVersion = "2.11.0"
    implementation("com.squareup.retrofit2:retrofit:${retrofitVersion}")
    implementation("com.squareup.retrofit2:converter-moshi:${retrofitVersion}")

    // moshi
    val moshiVersion = "1.15.1"
    implementation("com.squareup.moshi:moshi:${moshiVersion}")
    implementation("com.squareup.moshi:moshi-kotlin:${moshiVersion}")
}
```

### Retrofitインターフェースの定義
RetfofitでAPI通信をする際に必要となるインターフェースを作成します。
usernameを動的に設定できるようにしておきます。
(Retrofitの使い方はここでは触れません🙇)
```ApiService.kt
interface ApiService {
    @GET("users/{username}/repos")
    suspend fun getUserRepositories(@Path("username") username: String): List<UserRepository>
}

data class UserRepository(
    val id: Int,
    val name: String,
    @Json(name = "html_url")
    val url: String,
)
```

### API呼び出しの処理を実装
念の為、実装したAPI通信部分がしっかり動くのか確認しておきます。
今回はサンプル実装のため、MainActivityに全部処理を記載します。

``` MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com/")
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()

        val gitHubApiService = retrofit.create(ApiService::class.java)

        // RetrofitでGitHubのAPIを叩いて、レスポンスをログに出力する
        runBlocking(Dispatchers.IO) {
            val response = gitHubApiService.getUserRepositories("octocat")
            response.forEach {
                println(it)
            }
        }
    }
}
```
ログ出力をしてレスポンスで取得しリポジトリを１件ずつログ出力しています。

うまく動いている場合、Logcatに以下のようなログが出力されるかと思います。
```debug.log
22024-06-23 18:31:04.065  5736-5793  System.out              com.example.mockwebserversample      I  UserRepository(id=132935648, name=boysenberry-repo-1, url=https://github.com/octocat/boysenberry-repo-1)
2024-06-23 18:31:04.065  5736-5793  System.out              com.example.mockwebserversample      I  UserRepository(id=18221276, name=git-consortium, url=https://github.com/octocat/git-consortium)
2024-06-23 18:31:04.066  5736-5793  System.out              com.example.mockwebserversample      I  UserRepository(id=20978623, name=hello-worId, url=https://github.com/octocat/hello-worId)
2024-06-23 18:31:04.066  5736-5793  System.out              com.example.mockwebserversample      I  UserRepository(id=1296269, name=Hello-World, url=https://github.com/octocat/Hello-World)
2024-06-23 18:31:04.066  5736-5793  System.out              com.example.mockwebserversample      I  UserRepository(id=64778136, name=linguist, url=https://github.com/octocat/linguist)
2024-06-23 18:31:04.066  5736-5793  System.out              com.example.mockwebserversample      I  UserRepository(id=17881631, name=octocat.github.io, url=https://github.com/octocat/octocat.github.io)
2024-06-23 18:31:04.067  5736-5793  System.out              com.example.mockwebserversample      I  UserRepository(id=1300192, name=Spoon-Knife, url=https://github.com/octocat/Spoon-Knife)
2024-06-23 18:31:04.067  5736-5793  System.out              com.example.mockwebserversample      I  UserRepository(id=56271164, name=test-repo1, url=https://github.com/octocat/test-repo1)

```

ここまで用意できれば、環境準備としては完了です👏


## MockWebServerでAPI通信部分のUnit Testを作成する
さて、本題のMockWebServerを使ったUnit Testを作成していきたいと思います。

### 依存関係の追加
まずはGradleにMockWebServerの依存関係を追加します。
その他、テスト周りで必要になるものも合わせて追加します。

``` build.gradle.kt(app)
dependencies {
    ・
    ・
    ・
    // MockWebServer
    testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")

    // JUnit
    testImplementation("junit:junit:4.13.2")

    // Assert library
    testImplementation("com.google.truth:truth:1.1.3")

    // Coroutines
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
}
```

### テストクラスの作成
テストクラスを作成します。
内容としては、以下のように、準備 + 終了処理を追加します。
- setUpでMockWebServerやRetrofitの諸々を準備
- tearDownでMockWebServerを終了

```ApiServiceTest.kt
class ApiServiceTest {
    private lateinit var mockWebServer: MockWebServer
    private lateinit var apiService: ApiService

    @Before
    fun setUp() {
        mockWebServer = MockWebServer()
        mockWebServer.start()

        val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

        val retrofit = Retrofit.Builder()
            .baseUrl(mockWebServer.url(""))
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()

        apiService = retrofit.create(ApiService::class.java)
    }

    @After
    fun tearDown() {
        mockWebServer.shutdown()
    }
}

```

上記の記載内容を詳しくみていきます。

#### setUp
テスト前に実行されるsetUpでは、MockWebServerの準備や開始、Retrofitの用意を行っています。
**mockWebServer.start()** でMockWebServerを起動し、Retrofitのリクエストを受け取れるようにしています。
```ApiService.kt
        mockWebServer = MockWebServer()
        mockWebServer.start()
```

MainActivityとほとんど同じように、Retrofitの用意を行っています。
一部違うのが**baseUrl**に設定する内容です。
mockWebServer.url("")を指定することで、Retrofitのリクエストの向き先をMockWebServeに設定しています。
```ApiService.kt
        val moshi = Moshi.Builder()
            .add(KotlinJsonAdapterFactory())
            .build()

        val retrofit = Retrofit.Builder()
            .baseUrl(mockWebServer.url("")) // MockWebServeに向くように設定
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()

        apiService = retrofit.create(ApiService::class.java)
```

#### tearDown
テスト実行後に行われるtearDownでは、MockWebServerの停止を行っています。
``` ApiServiceTest.kt
fun tearDown() {
        mockWebServer.shutdown()
    }
```

### テストケースの作成
実際にMockWebServerのレスポンスを設定したテストケースを作成します。
```ApiServiceTest.kt
    @Test
    fun get_user_repositories_success() = runTest {
        val mockResponse = MockResponse()
            .setResponseCode(200)
            .setBody(
                """
                [
                    {
                        "id": 1,
                        "name": "repo1",
                        "html_url": "https://example.com/repo1"
                    },
                    {
                        "id": 2,
                        "name": "repo2",
                        "html_url": "https://example.com/repo2"
                    }
                ]
                """.trimIndent()
            )

        mockWebServer.enqueue(mockResponse)

        val response = apiService.getUserRepositories("octocat")

        assertThat(response).isNotNull()
        assertThat(response).hasSize(2)
    }
```
詳しくみていきます。

#### MockWebServerが返すレスポンスの設定
まず最初にやっているのが、MockWebServerが返すレスポンスの設定です。
ここではステータスコードを200に設定、レスポンスボディにレスポンスの内容(Json形式)を作成しています。
今回ボティに設定しているJsonの内容はテキトーに設定していますが、実際にテストを作成するときは実環境に近いものを設定した方がいいです。

mockWebServer.enqueue(mockResponse)を実行し、mockWebServerに作成したレスポンスを返すように設定します。

```ApiServiceTest.kt
        val mockResponse = MockResponse()
            .setResponseCode(200)
            .setBody(
                """
                [
                    {
                        "id": 1,
                        "name": "repo1",
                        "html_url": "https://example.com/repo1"
                    },
                    {
                        "id": 2,
                        "name": "repo2",
                        "html_url": "https://example.com/repo2"
                    }
                ]
                """.trimIndent()
            )

        mockWebServer.enqueue(mockResponse)
```

#### リクエストの実行 + 検証
下記の部分では、getUserRepositoriesを実行してリクエストを実行し、取得したレスポンスの内容を検証しています。
検証内容としては、レスポンスが問題なくConvertされて取得できているか、レスポンスにリストが２つあるかを確認し、リストの内容も確認しています。
``` ApiServiceTest.kt
        val response = apiService.getUserRepositories("octocat")

        assertThat(response).isNotNull()
        assertThat(response).hasSize(2)

        assertThat(response[0].id).isEqualTo(1)
        assertThat(response[0].name).isEqualTo("repo1")
        assertThat(response[0].url).isEqualTo("https://example.com/repo1")

        assertThat(response[1].id).isEqualTo(2)
        assertThat(response[1].name).isEqualTo("repo2")
        assertThat(response[1].url).isEqualTo("https://example.com/repo2")
```

全体の流れとしては以上のような感じです。

### テストケースの作成（失敗）
失敗するケースも作成してみます。
getUserRepositoriesを呼び出す時に引数で空文字を渡し、usernameの箇所を空文字でリクエストした時の動きを検証します。
成功時のリクエスト：https://api.github.com/users/octocat/repos
失敗時のリクエスト：https://api.github.com/users//repos

前述した成功するテストケースと同じようにレスポンスを作成し、検証しています。
今回はステータスコードに404を設定し、エラー時のレスポンをを返すように設定しています。
```ApiServiceTest.kt
    @Test
    fun get_user_repositories_error() = runTest {
        val mockResponse = MockResponse()
            .setResponseCode(404)

        mockWebServer.enqueue(mockResponse)

        try {
            apiService.getUserRepositories("") // 空文字を渡す
        } catch (e: Exception) {
            assertThat(e).isInstanceOf(Exception::class.java)
        }
    }

```


### レスポンスの返却内容をまとめて定義する
上記のようなテストケースでも十分かと思いますが、レスポンスの設定の処理が冗長になっています。
そんなときは**Dispater**を定義することで、リクエストをまとめてハンドリングすることが可能です。

#### setupでDispaterを設定
MockWebServerを準備していたsetupにDispaterを定義していきます。
下記はリクエストパスごとに異なるレスポンスを返すように設定しています。
``` ApiServiceTest.kt
    fun setUp() {
        mockWebServer = MockWebServer()
        val dispatcher = object : Dispatcher() {
            override fun dispatch(request: RecordedRequest): MockResponse {
                return when {
                    request.path!!.matches(Regex("/users/[a-zA-Z0-9]+/repos")) -> {
                        MockResponse().setResponseCode(200).setBody(
                            """
                            [
                                {
                                    "id": 1,
                                    "name": "repo1",
                                    "html_url": "https://example.com/repo1"
                                },
                                {
                                    "id": 2,
                                    "name": "repo2",
                                    "html_url": "https://example.com/repo2"
                                }
                            ]
                            """.trimIndent()
                        )
                    }

                    else -> {
                        MockResponse().setResponseCode(404)
                    }
                }
            }
        }

        mockWebServer.dispatcher = dispatcher
        mockWebServer.start()

        ・
        ・
        ・
    }

```
上記にようにsetUpでまとめることで、テストケースごとに設定していたレスポンスの設定は不要になり、すっきりさせることができます。

``` ApiServicetest.kt
    @Test
    fun get_user_repositories_success() = runTest {
        val response = apiService.getUserRepositories("octocat")
        assertThat(response).isNotNull()
        assertThat(response).hasSize(2)

        assertThat(response[0].id).isEqualTo(1)
        assertThat(response[0].name).isEqualTo("repo1")
        assertThat(response[0].url).isEqualTo("https://example.com/repo1")

        assertThat(response[1].id).isEqualTo(2)
        assertThat(response[1].name).isEqualTo("repo2")
        assertThat(response[1].url).isEqualTo("https://example.com/repo2")
    }

    @Test
    fun get_user_repositories_error() = runTest {
        try {
            apiService.getUserRepositories("")
        } catch (e: Exception) {
            assertThat(e).isInstanceOf(Exception::class.java)
        }
    }
```

## おわりに
今回はMockWebServerを使用し、API通信部分のUnit Testを作成する流れをまとめてみました。
API通信の部分のテスト、、割と複雑なのでは？と思っていたのですが、MockWebServerを使うことで、割と簡単にシュミレートしてテストを行うことができるんだなと思いました。

API通信のアプリの作成の際は、MockWebServerを導入してUnit Testを実装していきたいと思います。

## 参考
[Retrofit](https://square.github.io/retrofit/)
[OkHttp](https://square.github.io/okhttp/)
[MockWebServerのREADMER](https://github.com/square/okhttp/tree/master/mockwebserver)
[GitHub API](https://docs.github.com/ja/rest?apiVersion=2022-11-28)