[Android] OkHttpのMockWebServerを用いた、OkHttp + Retrofitのユニットテスト
目次
OkHttpのMockWebServerを用いて、OkHttpとRetrofitを用いたRESTful APIアクセス部分のユニットテストを書いていきます。Android Developersに掲載されているアプリアーキテクチャガイドにおける、Data layerのRemoteDataSourceにあたる部分です。
以下のサンプルコードのOkHttpとMockWebServerのバージョンは4.9.1、Retrofitのバージョンは2.9.0です(バージョンが少し古いのは、このサンプルコードを書いたのがしばらく前だったからです)。MockWebServerは testImplementation
としています。
dependencies {
implementation("com.squareup.okhttp3:okhttp:4.9.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.0")
implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0")
testImplementation("com.squareup.okhttp3:mockwebserver:4.9.1")
}
MockWebServerを用いた、OkHttpとRetrofitを用いたRESTful APIアクセス部分のユニットテスト
はじめにテストクラスの最終型を示します。サンプルコードの元になったのが自分の個人プロジェクトなので、趣味に走っているところがあります。それについては後段で説明を加えていきます。ポイントは、 @Before
と @After
でMockWebServerの起動と停止をおこなっていること、各テストケースでMockWebServerのレスポンスコードとレスポンスボディを enqueue
していることになるでしょうか。
class RemoteDataSourceImplTest {
private val mockWebServer = MockWebServer()
private lateinit var target: RemoteDataSource
@ExperimentalSerializationApi
@Before
fun setUp() {
mockWebServer.start()
target = RemoteDataSourceImpl(
apiClient = TestApiClientProvider().provideWith(mockWebServer),
)
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun testGetAwesomeData_returnRightIfSuccess() = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(dummyJson()))
val actual = target.getAwesomeData()
assert(actual is Either.Right && actual.value.items.isNotEmpty())
}
@Test
fun testGetAwesomeData_returnLeftIfFailure() = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(500))
val actual = target.getAwesomeData()
assert(actual is Either.Left)
}
}
private fun dummyJson(): String =
"""
{
"items": [
{
"id": 1,
"name": "Starbucks",
"created_at": "2022-01-02T05:48:15.209Z",
"updated_at": "2022-01-02T05:48:15.209Z"
},
{
"id": 2,
"name": "Komeda Coffee",
"created_at": "2022-01-02T05:48:46.648Z",
"updated_at": "2022-01-02T05:48:46.648Z"
},
{
"id": 3,
"name": "Mr.Donuts",
"created_at": "2022-01-02T05:49:16.757Z",
"updated_at": "2022-01-02T05:49:16.757Z"
}
]
}""".trimIndent()
コードの説明を以下に示します
MockWebServerの起動と停止
MockWebServerの起動と停止は以下の箇所で行っています。テストケースごとにMockWebServerの起動と停止を行うため、 @Before
で MockWebServer#start
を、 @After
で MockWebServer#shutdown
を呼んでいます。
private val mockWebServer = MockWebServer()
@ExperimentalSerializationApi
@Before
fun setUp() {
mockWebServer.start()
// 省略
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
MockWebServerへのレスポンスのenqueue
以下の箇所では、MockWebServerにモックレスポンスをenqueueしています。MockWebServerにリクエストを行った時に、queueからdequeueされたモックレスポンスを返してくれます。テストケースに合わせてモックレスポンスを作ってあげることで、RESTful APIアクセス部分のユニットテストを書くことができます。
@Test
fun testGetAwesomeData_returnRightIfSuccess() = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(dummyJson()))
// 略
}
@Test
fun testGetAwesomeData_returnLeftIfFailure() = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(500))
// 略
}
上記の1個目のテストケースでは、リクエストに成功した場合(レスポンスコードが200番台)の時のテストを行っています。レスポンスボディーには、後述するJsonをrawStringで定義した値を渡しています。
上記の2個目のテストケースでは、リクエストに失敗した場合(レスポンスコードが200番台でない)のテストを行っています。レスポンスコードを適宜設定することで、レスポンスコードに応じた挙動のテストを記述できます。また、1個目の例と同様に setBody
を呼び出すことで、エラーレスポンスを定義することもできます。
Json
レスポンスのJsonはKotlinのrawStringで定義しました。Jsonのファイルをプロジェクト内に置き、ファイルからJsonの文字列を読み込む方法もあると思います。
private fun dummyJson(): String =
"""
{
"items": [
{
"id": 1,
"name": "Starbucks",
"created_at": "2022-01-02T05:48:15.209Z",
"updated_at": "2022-01-02T05:48:15.209Z"
},
{
"id": 2,
"name": "Komeda Coffee",
"created_at": "2022-01-02T05:48:46.648Z",
"updated_at": "2022-01-02T05:48:46.648Z"
},
{
"id": 3,
"name": "Mr.Donuts",
"created_at": "2022-01-02T05:49:16.757Z",
"updated_at": "2022-01-02T05:49:16.757Z"
}
]
}""".trimIndent()
Retrofit
Retrofitを用いた、RESTful APIのクライアントは以下のように作っています。クライアントのinterfaceはApiClientという名前にしました。APIアクセスを行う関数については、データをRetrofitのResponseに包んで返すsuspend関数として定義しています。Kotlin Coroutinesではなく、例えばRxJavaのSingleを使うような場合でも、全体的な実装の流れは変わらないと思います。
interface ApiClient {
@GET("/")
suspend fun getAwesomeData(): Response<AwesomeData>
}
ApiClientの注入
テスト対象にApiClientを注入していますが、そのインスタンスは以下のような処理で生成するようにしています。このサンプルコードではJsonのパーサとしてKotlin Serializationを使っていますが、例えばMoshiを使う時なども同様のインスタンス生成のコードを書けると思います。
@ExperimentalSerializationApi
internal class TestApiClientProvider {
fun provideWith(mockWebServer: MockWebServer): ApiClient =
Retrofit.Builder()
.baseUrl(mockWebServer.url(""))
.client(OkHttpClient())
.addConverterFactory(converterFactory())
.build()
.create(ApiClient::class.java)
private val json = Json { ignoreUnknownKeys = true }
private fun converterFactory(): Converter.Factory =
json.asConverterFactory(contentType = "application/json".toMediaType())
}
テスト対象のクラス
参考のために、テスト対象のクラスの定義も記載しておきます。Either型を返すsuspend funのみを定義したinterfaceです。Either型については、 Either / Option in Kotlin に書きました。簡単に言うと、成功をRight型、失敗をLeft型で表すsealed classです。
interface RemoteDataSource {
suspend fun getAwesomeData(): Either<AwesomeException, Awesomedata>
}
その実装クラスは以下です。前述のApiClientをコンストラクタで注入しています。RetrofitのResponse型をEither型に変換して返しています(Retrofitへの依存をRemoteDataSourceImplの外に持ち出さないようにするためです)。
class RemoteDataSourceImpl @Inject constructor(
private val apiClient: ApiClient,
) : RemoteDataSource {
override suspend fun getAwesomeData(): Either<AwesomeException, Awesomedata> =
apiClient.getAwesomeData().toEither()
}
RetrofitのResponse型は、レスポンスコードが200番台の時に isSuccessful
がtrueを返します。このサンプルコードでは、単純に、 isSuccessful
がtrueの時はEither.Right、falseの時はEither.Leftを返すようにしています。
private fun <T : Any> Response<T>.toEither(): Either<AwesomeException, T> =
if (isSuccessful) {
Either.Right(requireNotNull(body())
} else {
Either.Left(AwesomeException())
}
Reference
- Data layer | Android Developers(最終アクセス日:2022年1月7日)
- Either / Option in Kotlin(最終アクセス日:2022年1月10日)
- [Kotlin] sealed classに親しむ(最終アクセス日:2022年1月20日)
書いている人 😎

茨城県つくば市在住のモバイルアプリケーションアーキテクト(Androidが得意です)。モバイルアプリのアーキテクチャ、自動テスト、CI/CDに興味があります。いわゆる「レガシーコード」のリファクタリング・リアーキテクチャが好きです。
👉 もっと詳しく
著書 ✍
Android 依存性注入 ヒッチハイク・ガイド🧳
Androidアプリでの依存性注入(Dependency Injection)に入門するためのガイダンスです。依存性注入の概念やメリットを理解し、Dagger Hiltを用いてAndroidアプリに適用する方法を解説しています。
ソフトウェアデザイン 2023年6月号📚
特集「クリーンアーキテクチャとは何か?」の第5章「モバイルアプリ開発における実践」を執筆しました。
Android クリーンアーキテクチャ ヒッチハイク・ガイド🧳
Androidアプリでのクリーンアーキテクチャに入門するためのガイダンスです。クリーンアーキテクチャの概念を理解し、Androidアプリに適用する方法を解説しています。
Android ユニットテスト ヒッチハイク・ガイド🧳
Androidアプリのユニットテストに入門するためのガイダンスです。初学者が混乱せずにAndroidアプリのユニットテストを書き始めることができる、ということを目的としています。
Android MVVMアーキテクチャ入門🛠
Androidアプリ開発の初学者に向けた、MVVM(Model-View-ViewModel)アーキテクチャの入門書を書きました。初学者の方を確実にネクストレベルに引き上げる技術書です。NextPublishingより出版されています。
関連記事 👀
- Truthのカスタムサブジェクトを定義する
- Mockk によるモック入門
- Truth によるアサーション入門
- 「Android ユニットテスト ヒッチハイク・ガイド」を頒布します
- [Android] Robolectricとandroidx.test