RxAndroidを利用しているソースでJVMでの単体テストをする方法

現象

普通に作っていると AndroidSchedulers.mainThread() の部分で ExceptionInInitializerErrorになります。

何故エラーになるのか

これは、AndroidSchedulers.mainThread() によって返されるスケジューラが、Androidに依存しているためです。(Androidに依存していると何もしなければ、エミュレーターを使いAndroidTestで実行する必要があります。)
このリンク先が該当のソースになります。

解決方法

単純にテストが実行される前に、RxAndroidPlugins機構により別のSchedulerとして初期化することで、この問題を回避できます。

具体的なソース

以下kotlinのソースです。単純にPlugin機構を使って、利用するSchedulerをAndroidに依存しないようにしているだけのTestRuleを定義します。

RxImmediateSchedulerRule.kt
class RxImmediateSchedulerRule : TestRule {

    private val immediate = object : Scheduler() {
        override fun scheduleDirect(run: Runnable, delay: Long, unit: TimeUnit): Disposable {
            return super.scheduleDirect(run, 0, unit)
        }

        override fun createWorker(): Worker {
            return ExecutorScheduler.ExecutorWorker { it.run() }
        }
    }

    override fun apply(base: Statement, description: Description): Statement {
        return object : Statement() {
            @Throws(Throwable::class)
            override fun evaluate() {
                RxJavaPlugins.setInitIoSchedulerHandler({ _ -> immediate })
                RxJavaPlugins.setInitComputationSchedulerHandler({ _ -> immediate })
                RxJavaPlugins.setInitNewThreadSchedulerHandler({ _ -> immediate })
                RxJavaPlugins.setInitSingleSchedulerHandler({ _ -> immediate })
                RxAndroidPlugins.setInitMainThreadSchedulerHandler({ _ -> immediate })

                try {
                    base.evaluate()
                } finally {
                    RxJavaPlugins.reset()
                    RxAndroidPlugins.reset()
                }
            }
        }
    }
}

そして、これをテストの前に実行するだけです。ちなみに@ClassRuleを使わない理由は、こちらの記事を参照してください。

@RunWith(PowerMockRunner::class)
@PrepareForTest(Auth::class)
class LoginViewModelTest {

    @Rule
    val schedulers: RxImmediateSchedulerRule = RxImmediateSchedulerRule()

//    companion object {
//        @JvmField
//        @ClassRule
//        val schedulers: RxImmediateSchedulerRule = RxImmediateSchedulerRule()
//    }
    @Test
    fun onClickLogin() {

        val mockAuth = PowerMockito.mock(Auth::class.java)

        val target = LoginViewModel(mockAuth)

        target.mail.set("email")
        target.password.set("password")

        val result = Single.just(AuthEntity().apply {
            accessToken = "123456"
            userId = 100
        })
        PowerMockito.`when`(mockAuth.login("email", "password")).thenReturn(result)

        target.onClickLogin().run()

        Mockito.verify(mockAuth).login("email", "password")
    }
}