com.google.android.gms.tasks.Task<T>のユニットテスト

com.google.android.gms.tasks.Task<T>にまつわり処理でユニットテストを書きたいということがある。例えばFirebaseを使うときにそんな思いに駆られることがあるだろう。

私はFirebase Realtime Databaseの処理をラップするクラスを作成したときに、ユニットテストを書きたいと思った。

何らかの処理を要求した後、返ってくるのがこのTask<T>というクラスである。リクエストはすべて非同期で処理されるので、処理の結果はaddOnSuccessListenerなどを使ってリスナーをセットして、そのリスナーの中でリクエストの結果を受け取るようなコードになる。

ユニットテスト書くときに困るポイントは2つ。

  • どうやって`Task`を生成するか
  • 処理結果をリスナーで受け取るにはどうするか

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つ、この場合は成功時に行う処理だけ指定していると思う。

この場合、OnSuccessListeneronSuccessメソッドがメインスレッドで呼び出されることに注意が必要である。

実はTask<T>に対して設定するOnSuccessListenerOnFailureListenerOnCompliteListenerはそれぞれ、特に指定を行わない限り必ず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>にリスナーを設定しようとも、タイミングによって呼ばれないことが起こりうるので、リスナーの呼び出しのテストを行う場合には必ず待ち合わせの処理を挟むことが必要。

Amazonのほしいものリストを公開しています。仕事で欲しいもの、単なる趣味としてほしいもの、リフレッシュのために欲しいものなどを登録しています。 寄贈いただけると泣いて喜びます。大したお礼はできませんが、よりよい情報発信へのモチベーションに繋がりますので、ご検討いただければ幸いです。