com.google.android.gms.tasks.Task<T>のユニットテスト
com.google.android.gms.tasks.Task<T>
にまつわり処理でユニットテストを書きたいということがある。例えばFirebaseを使うときにそんな思いに駆られることがあるだろう。
私はFirebase Realtime Databaseの処理をラップするクラスを作成したときに、ユニットテストを書きたいと思った。
何らかの処理を要求した後、返ってくるのがこのTask<T>
というクラスである。リクエストはすべて非同期で処理されるので、処理の結果はaddOnSuccessListener
などを使ってリスナーをセットして、そのリスナーの中でリクエストの結果を受け取るようなコードになる。
ユニットテスト書くときに困るポイントは2つ。
- どうやって`Task
`を生成するか - 処理結果をリスナーで受け取るにはどうするか
Task<T>
を生成するにはTasks.forResult()
などのメソッドを利用するとよい。
com.google.android.gms.tasks.Tasks
がファクトリメソッドみたいなものなので、こいつ経由で目当てのTask<T>
が作れる。こうすることで起点部分の問題はクリアできる。
https://developers.google.com/android/reference/com/google/android/gms/tasks/Tasks
処理結果をリスナーで受け取る際は注意が必要である。
利用する際にはaddOnSuccessListener()
に渡す引数は1つ、この場合は成功時に行う処理だけ指定していると思う。
この場合、OnSuccessListener
のonSuccess
メソッドがメインスレッドで呼び出されることに注意が必要である。
実はTask<T>
に対して設定するOnSuccessListener
、OnFailureListener
、OnCompliteListener
はそれぞれ、特に指定を行わない限り必ずAndroidのメインスレッド上で呼び出される。このメインスレッドで呼び出されるというのは、Looper.getMainLopper()
で取得されるメインスレッドである。
つまり、JVMユニットテスト(testディレクトリに配置するユニットテスト)ではリスナーの処理が呼び出されないことに注意が必要である。
この問題に対応するためには、Instrumentation testとして実行する(androidTestディレクトリに配置する)か、addOnSuccessListener
でリスナーを設定する際に、スレッドを指定(Executor
を指定)してやればよい。
val executor: Executor = Executors.newSingleThreadExecutor()
Tasks.forResult("test")
.addOnSuccessListener(executor, OnSuccessListener { println(it) })
ちなみにそのままでは処理タイミングの問題で、リスナーの処理が呼び出される前にテストメソッドが終了してしまう場合がある。そのため、CountDownLatch
などを利用するなどして、リスナーの処理が呼び出されるのを待ち合わせたりする必要がある。
待ち合わせが必要になるのは、Instrumentation Testでも同じである。たとえTasks.forResult()
を使って即座に処理が完了するTask<T>
にリスナーを設定しようとも、タイミングによって呼ばれないことが起こりうるので、リスナーの呼び出しのテストを行う場合には必ず待ち合わせの処理を挟むことが必要。