1. 始める前に
前の Codelab では、Retrofit を使用してネットワーク リクエストを行う方法を学習しました。この Codelab では、ネットワーク コードの単体テストを作成する方法を学習します。
前提条件
- Android Studio でテスト ディレクトリを作成している
- Android Studio で単体テストとインストルメンテーション テストを作成している
- Android プロジェクトに Gradle の依存関係を追加している
学習内容
- テスト用のリソースを保存する方法
- テスト用の API レスポンスをモックする方法
- Retrofit API サービスをテストする方法
必要なもの
- Android Studio がインストールされているパソコン
- Mars Photos アプリの解答コード
この Codelab のスターター コードをダウンロードする
この Codelab では、Mars Photos アプリの解答コードにインストルメンテーション テストを追加します。
この Codelab のコードを取得して Android Studio で開く手順は以下のとおりです。
コードを取得する
- 指定された URL をクリックします。プロジェクトの GitHub ページがブラウザで開きます。
- プロジェクトの GitHub ページで、[Code] ボタンをクリックすると、ダイアログが表示されます。
- ダイアログで、[Download ZIP] をクリックして、プロジェクトをパソコンに保存します。ダウンロードが完了するまで待ってください。
- パソコンに保存したファイルを見つけます([ダウンロード] フォルダなど)。
- ZIP ファイルをダブルクリックして展開します。プロジェクト ファイルが入った新しいフォルダが作成されます。
Android Studio でプロジェクトを開く
- Android Studio を起動します。
- [Welcome to Android Studio] ウィンドウで [Open an existing Android Studio project] をクリックします。
注: Android Studio がすでに開いている場合は、メニューから [File] > [New] > [Import Project] を選択します。
- [Import Project] ダイアログで、展開したプロジェクト フォルダがある場所([ダウンロード] フォルダなど)に移動します。
- そのプロジェクト フォルダをダブルクリックします。
- Android Studio でプロジェクトが開くまで待ちます。
- 実行ボタン をクリックし、アプリをビルドして実行します。正常にビルドされたことを確認します。
- [Project] ツール ウィンドウでプロジェクト ファイルを見て、アプリがどのように設定されているかを確認します。
2. スターター アプリの概要
Mars Photos アプリは、ネットワーク リクエストを使用して取得した写真のリストを表示する単一の画面で構成されています。
このスターター コードには、テストに関する微妙な違いが付加されています。この Codelab の範囲外のため詳しくは説明しませんが、この点は重要です。
アプリが API からデータを取得するプロセスをテストする際には、データがどのように表示されるかを確認できるように、常に独自のデータを提供することをおすすめします。API からのデータが変更された場合、テストが中断され、不必要に失敗することがあります。また、実際のネットワーク呼び出しに依存していると、ネットワーク接続やネットワーク速度が原因で障害が生じ、テストの整合性が損なわれる可能性があります。そのため、テストでデータを生成し、test/res ディレクトリに JSON ファイルとして保存します。これはリソース ディレクトリで、メインアプリのコードのリソース ディレクトリに似ていますが、テストリソースが test/res に格納される点が異なります。
test/res
ディレクトリを追加します。プロジェクト ビューでsrc
ディレクトリを右クリックし、プルダウンから [New] -> [Directory] を選択します。
- 表示されたポップアップで下にスクロールして、
test/res
を選択します。
- プロジェクト ビューで
test/res
ディレクトリを右クリックし、[New] -> [File] を選択します。
- ファイル名を
mars_photos.json
とします。
- 次のコードを
mars_photos.json
ファイルにコピーします。
[
{
"id":"424905",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000MR0044631300503690E01_DXXX.jpg"
},
{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
}
]
- このリソース ディレクトリのファイルにアクセスするには、ビルドファイルでテストリソース ディレクトリを「ソース」ディレクトリとして明示的に指定する必要があります。それには次の行を追加します。
app/build.gradle
android {
...
sourceSets {
test.resources.srcDirs += 'src/test/res'
}
}
これにより、テストでアクセスするたびにファイルへのフルパスをコードに入力することなく、リソース ファイルにアクセスできるようになります。ファイルパスはマシンとオペレーティング システムの間で変更される可能性があるため、テストでフルパスを入力することはおすすめしません。
- 同じファイルに次の依存関係を追加し、Gradle を同期します。
app/build.gradle
dependencies {
...
testImplementation 'junit:junit:4.12'
testImplementation "androidx.arch.core:core-testing:2.1.0"
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
}
- 通常のテスト ディレクトリを作成します。
- テスト ディレクトリで、新しいパッケージ
com.example.android.marsphotos
を作成します。 - パッケージ内で、
BaseTest.kt
という名前の新しい Kotlin ファイルを作成します。
- 次のコードをコピーして
BaseTest.kt
ファイルに貼り付けます。このようなコードは見たことがないかもしれませんが、問題ありません。以下のコードでは「Mock」という用語が繰り返し使用されていますが、これについてはこの Codelab で説明します。このクラスのenqueue()
関数は、作成したmars_photos.json
ファイルのデータを解析して、後で作成するテストで使用できるようにします。
BaseTest.kt
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okio.buffer
import okio.source
open class BaseTest {
val mockWebServer = MockWebServer()
fun enqueue(fileName: String) {
val inputStream = javaClass.classLoader!!.getResourceAsStream(fileName)
val buffer = inputStream.source().buffer()
mockWebServer.enqueue(
MockResponse()
.setResponseCode(200)
.setBody(buffer.readString(Charsets.UTF_8))
)
}
}
3.データのモック
この Codelab では、データのモックに大きく依存します。テストにおけるモックとは、コードの戻り値をシミュレートすることを意味します。前の Codelab では、クラスをモックしました。また、関数をモックして指定した値を返したり、API をモックして指定したデータを返したりすることもできます。モックすることでテスト対象のコードを分離できるため、テストに役立ちます。この Codelab では、API レスポンスのモックに焦点を当てます。関数のモックについては別の Codelab で説明します。
4. 単体テストクラスを作成する
このテストでは API サービスをテストします。まず、MarsApiServiceTests.kt という名前の新しいクラスを作成します。
5. 依存関係
この Codelab のスターター コードには必要な依存関係が含まれていますが、まだ説明していない重要な依存関係が存在します。
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.1"
この依存関係により、モックサーバーを作成できます。基本的に、モックサーバーはネットワーク リクエストをインターセプトし、定義したモックデータを返すようにルートを変更します。モックの意味については、この Codelab の後半で説明します。
6. おすすめの方法
このテストは、オブジェクト指向のプログラミング言語である Kotlin で記述します。つまり、テスト用のオブジェクト指向コードを記述できます。このテスト用に記述するコード量はそれほど多くないため、本来はオブジェクト指向の手法を使用する必要はありませんが、ここではコンセプトを説明するために実装します。
スターター コードにすでに単体テスト ディレクトリが含まれており、その中に BaseTest
というクラスがあることにお気づきでしょうか。複数のテストに使用するコードがある場合は、open class
を作成してテストクラスに継承させることができます。この BaseTest クラスには enqueue()
というメソッドが 1 つしかないため、この Codelab では 1 つのテストクラスのみを作成します。独自のテストを作成するときは、オブジェクト指向プログラミングを使用できることを忘れないでください。ただし、必要のない場合に使いすぎないことが重要です。
7. ネットワーク リクエストのテストを作成する
まず、テストクラスが BaseTest
から継承していることを確認します。
MarsApiService
を直接テストするため、そのインスタンスが必要です。アプリコードの場合と同じように設定しますが、この新しいサービスの URL は異なります。
- テストクラスで、
MarsApiService
のlateinit
変数を作成します。
private lateinit var service: MarsApiService
次に、すべてのテストの前に service
変数を定義する関数が必要です。
setup()
という関数を作成し、@Before
アノテーションを付けます。
@Before
fun setup() {}
このテストでは、ネットワーク リクエストに URL を使用しません。そこで役立つのが MockWebServer
です。
データのモック
BaseTest
クラスには、MockWebServer
オブジェクトのインスタンスである mockWebServer,
というプロパティがあります。このオブジェクトはネットワーク リクエストをインターセプトしますが、まず、インターセプトする URL にネットワーク リクエストをルーティングする必要があります。
MockWebServer
には、インターセプトする URL を指定する url()
という関数があります。ここでは実際のネットワーク リクエストは作成しません。テスト自体で制御するデータを使ってネットワーク コードをテストできるように、擬似的にネットワーク リクエストを作成します。url()
関数はその疑似 URL を表す文字列を受け取り、HttpUrl
オブジェクトを返します。setup()
関数に、次の行を記述します。
val url = mockWebServer.url("/")
インターセプトする URL エンドポイントを設定し、返された HttpUrl
オブジェクトを取得しました。
この関数内で、MarsApiService
と MarsApiClass
の場合と同じ方法で(lazy
変数を除く)MarsApiService
のインスタンスを作成します。ベース URL はまだ含めないでください。コードは次のようになります。
service = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
))
.build()
.create(MarsApiService::class.java)
次に、ベース URL を設定します。Retrofit.Builder()
の関数チェーンに次のコードを追加します。
.baseUrl(url)
これにより、リクエストを MockWebServer
にルーティングすることを API サービスに示します。
service = Retrofit.Builder()
.baseUrl(url)
.addConverterFactory(MoshiConverterFactory.create(
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
))
.build()
.create(MarsApiService::class.java)
ここでも、MockWebServer
のポイントは、実際の API に対して実際のネットワーク リクエストを行わないようにすることです。基本的な考え方は、実際のネットワーク リクエストが行われて API が失敗した場合、テストが不合格になるということです。実際の API を使用する場合、API 自体をテストすることになりますが、ここでの目的はあくまで、Android プロジェクトでコードをテストすることです。
MockWebServer
は、作成したデータを返す疑似 API と考えることができます。そのため、リクエストを行う前に、何を返すべきかを MockWebServer
に明示的に指定する必要があります。そこで出てくるのが、BaseTest
の enqueue()
関数です。この関数のコードはこの Codelab の範囲外ですので、心配する必要はありません。この関数が、テストリソースからファイルを取得して、疑似 API レスポンスに変換することだけを理解しておいてください。
api_service()
というテスト関数を作成します。テスト関数で、次のようにenqueue
メソッドを呼び出します。
enqueue("mars_photos.json")
- 次に、
MarsApiService
から直接getPhotos()
関数を呼び出します。getPhotos()
は suspend 関数であり、コルーチン スコープから呼び出す必要があることにご注意ください。そのためには、次のように、runBlocking
のメソッドの呼び出しをラップします。
runBlocking {
val apiResponse = service.getPhotos()
}
- ここで、
getPhotos()
レスポンスがnull
でないことを確認します。apiResponse
をrunBlocking
内で定義したため、runBlocking
内でもアクセスする必要があります。
runBlocking {
val apiResponse = service.getPhotos()
assertNotNull(apiResponse)
}
getPhotos()
は MarsPhoto
オブジェクトを含むリストを返すため、リストが期待どおりのサイズであることを確認します。
- test/res の mars_photos.json ファイルを調べます。リストが空でないことを確認するために、別のアサーションを作成します。また、データの一部が正しいことを確認するためのアサーションも作成します。test/res/mars_photos.json に移動し、いずれかの ID をコピーします。この ID の値が、対応するリストアイテムの ID の値と等しいことのアサーションを行います。
runBlocking {
val apiResponse = service.getPhotos()
assertNotNull(apiResponse)
assertTrue("The list was empty", apiResponse.isNotEmpty())
assertEquals("The IDs did not match", "424905", apiResponse[0].id)
}
テストコードは次のようになります。
8. 解答コード
9. 完了
ネットワーク リクエストのテストは非常に複雑になる場合があり、この Codelab で学習した内容はほんの一部にすぎません。アプリがデータを取得する API ごとに、固有のネットワーク テストを作成する必要があります。API の操作に慣れてきたら、テストを拡張してネットワーク障害やさまざまな API レスポンスをシミュレートできます。これはテストの中でも非常に難しい領域ですが、エキスパートになるには試行錯誤が必要です。ぜひ練習を続けてください。
この Codelab では次のことを学びました。
- オブジェクト指向プログラミングをテストに適用する方法
- テスト ディレクトリにリソースを格納する方法
- テストで suspend 関数を呼び出す方法
- テストで API レスポンスをモックする方法