java.lang.ExceptionInInitializerError
RxJava/RxAndroidの非同期テストを書いていたときに、「 java.lang.ExceptionInInitializerError 」というエラーが発生しました。
java.lang.ExceptionInInitializerError
at io.reactivex.android.schedulers.AndroidSchedulers$1.call(AndroidSchedulers.java:35)
at io.reactivex.android.schedulers.AndroidSchedulers$1.call(AndroidSchedulers.java:33)
at io.reactivex.android.plugins.RxAndroidPlugins.callRequireNonNull(RxAndroidPlugins.java:70)
at io.reactivex.android.plugins.RxAndroidPlugins.initMainThreadScheduler(RxAndroidPlugins.java:40)
Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
at android.os.Looper.getMainLooper(Looper.java)
at io.reactivex.android.schedulers.AndroidSchedulers$MainHolder.<clinit>(AndroidSchedulers.java:29)
... 41 more
原因
皆さんよく書くと思われる .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
の部分で
AndroidSchedulers.mainThread()
がAndroidに依存したクラスを利用しているから、とのことです。
参考
https://stackoverflow.com/questions/43356314/android-rxjava-2-junit-test-getmainlooper-in-android-os-looper-not-mocked-runt
https://qiita.com/ko2ic/items/397fd147d4f3d0bdca09
対策
RxJavaPlugins/RxAndroidPluginsを使用する。
下記のような処理をテストの @Before
などに書くと、Schedulerを指定することが出来、エラーを回避できました。
val immediate = object : Scheduler() {
override fun scheduleDirect(run: Runnable, delay: Long, unit: TimeUnit): Disposable {
return super.scheduleDirect(run, 0, unit)
}
override fun createWorker(): Scheduler.Worker {
return ExecutorScheduler.ExecutorWorker(Executor { it.run() })
}
}
RxJavaPlugins.setInitIoSchedulerHandler { immediate }
RxJavaPlugins.setInitComputationSchedulerHandler { immediate }
RxJavaPlugins.setInitNewThreadSchedulerHandler { immediate }
RxJavaPlugins.setInitSingleSchedulerHandler { immediate }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediate }
さらなる問題
これで大丈夫と思いきや、さらなる問題が…
非同期処理が成功したあとに、続けて非同期処理を行うと、2回目の非同期処理が永遠に終わらない状態に。
(この対策とは関係のない部分が原因の可能性も否定できませんが…)
対策(2)
テスト時だけ、
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
を
.subscribeOn(Schedulers.trampoline()).observeOn(Schedulers.trampoline())
に変更。
Schedulerを変更させちゃえばいい、という発想ですね。
DroidKaigiのアプリを見てみると、Repositoryが SchedulerProvider
をInjectするようになっており、テスト時には TestSchedulerProvider
というのを注入する形になっていました。
https://github.com/DroidKaigi/conference-app-2018/blob/master/app/src/main/java/io/github/droidkaigi/confsched2018/data/repository/ContributorDataRepository.kt#L19
確かにRxJavaPlugins/RxAndroidPluginsで変更させるのは若干の黒魔術感があるので、DIで変えてあげるほうがいいのかもしれないと思いました。
結論
・Schedulerを変更させる必要があります。
・DIでSchedulerを注入させて、テスト時は Schedulers.trampoline()
にする、のがいいかもしれない。