RxJava 2 переопределяет планировщик ввода-вывода в unit test

Я пытаюсь протестировать следующий код RxKotlin/RxJava 2:

validate(data)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .flatMap { ... }

Я пытаюсь переопределить планировщики следующим образом:

// Runs before each test suite
RxJavaPlugins.setInitIoSchedulerHandler { Schedulers.trampoline() }
RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }

Однако при запуске теста я получаю следующую ошибку:

java.lang.ExceptionInInitializerError
...
Caused by: java.lang.NullPointerException: Scheduler Callable result can't be null
    at io.reactivex.internal.functions.ObjectHelper.requireNonNull(ObjectHelper.java:39)
    at io.reactivex.plugins.RxJavaPlugins.applyRequireNonNull(RxJavaPlugins.java:1317)
    at io.reactivex.plugins.RxJavaPlugins.initIoScheduler(RxJavaPlugins.java:306)
    at io.reactivex.schedulers.Schedulers.<clinit>(Schedulers.java:84)

Кто-нибудь испытал эту проблему?


Тест работал нормально при использовании RxKotlin/RxJava 1 и переопределяет следующий планировщик:

RxAndroidPlugins.getInstance().registerSchedulersHook(object : RxAndroidSchedulersHook() {
    override fun getMainThreadScheduler() = Schedulers.immediate()
})

RxJavaPlugins.getInstance().registerSchedulersHook(object : RxJavaSchedulersHook() {
    override fun getIOScheduler() = Schedulers.immediate()
})

Спасибо!

Ответ 1

Я предлагаю вам использовать другой подход и добавить слой абстракции к вашим планировщикам. У этого парня хорошая статья: https://medium.com/@peter.tackage/an-alternative-to-rxandroidplugins-and-rxjavaplugins-scheduler-injection-9831bbc3dfaf

В Котлине это выглядело бы как-то так:

interface SchedulerProvider {
    fun ui(): Scheduler
    fun computation(): Scheduler
    fun trampoline(): Scheduler
    fun newThread(): Scheduler
    fun io(): Scheduler }

И затем вы переопределите это с помощью собственной реализации SchedulerProvider:

class AppSchedulerProvider : SchedulerProvider {
    override fun ui(): Scheduler {
        return AndroidSchedulers.mainThread()
    }

    override fun computation(): Scheduler {
        return Schedulers.computation()
    }

    override fun trampoline(): Scheduler {
        return Schedulers.trampoline()
    }

    override fun newThread(): Scheduler {
        return Schedulers.newThread()
    }

    override fun io(): Scheduler {
        return Schedulers.io()
    }
}

И один для тестирования классов:

class TestSchedulerProvider : SchedulerProvider {
    override fun ui(): Scheduler {
        return Schedulers.trampoline()
    }

    override fun computation(): Scheduler {
        return Schedulers.trampoline()
    }

    override fun trampoline(): Scheduler {
        return Schedulers.trampoline()
    }

    override fun newThread(): Scheduler {
        return Schedulers.trampoline()
    }

    override fun io(): Scheduler {
        return Schedulers.trampoline()
    }
}

Ваш код будет выглядеть так, как вы называете RxJava:

mCompositeDisposable.add(mDataManager.getQuote()
        .subscribeOn(mSchedulerProvider.io())
        .observeOn(mSchedulerProvider.ui())
        .subscribe(Consumer<Quote> {
...

И вы просто переопределите свою реализацию SchedulerProvider на основе того, где вы ее протестируете. Вот пример проекта для ссылки, я связываю тестовый файл, который будет использовать тестируемую версию SchedulerProvider: https://github.com/Obaied/DingerQuotes/blob/master/app/src/test/java/com/obaied/dingerquotes/QuotePresenterTest.kt#L31

Ответ 2

Подумал! Это связано с тем, что в этом коде:

validate(data)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .flatMap { ... }

validate(data) возвращал Observable, который испускал следующее: emitter.onNext(null). Поскольку RxJava 2 больше не принимает значения null, flatMap не вызывался. Я изменил validate, чтобы вернуть Completable, и обновил переопределение планировщика на следующее:

RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }

Теперь тесты проходят!

Ответ 3

Это точный синтаксис, который работал для меня:

RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline())

Ответ 4

В качестве альтернативы предлагаемым решениям, это работало некоторое время в моих проектах. Вы можете использовать его в своих тестовых классах, например так:

@get:Rule
val immediateSchedulersRule = ImmediateSchedulersRule()

И класс выглядит так:

class ImmediateSchedulersRule : ExternalResource() {

    val immediateScheduler: Scheduler = object : Scheduler() {

        override fun createWorker() = ExecutorScheduler.ExecutorWorker(Executor { it.run() })

        // This prevents errors when scheduling a delay
        override fun scheduleDirect(run: Runnable, delay: Long, unit: TimeUnit): Disposable {
            return super.scheduleDirect(run, 0, unit)
        }

    }

    override fun before() {
        RxJavaPlugins.setIoSchedulerHandler { immediateScheduler }
        RxJavaPlugins.setComputationSchedulerHandler { immediateScheduler }
        RxJavaPlugins.setNewThreadSchedulerHandler { immediateScheduler }

        RxAndroidPlugins.setInitMainThreadSchedulerHandler { immediateScheduler }
        RxAndroidPlugins.setMainThreadSchedulerHandler { immediateScheduler }
    }

    override fun after() {
        RxJavaPlugins.reset()
    }

}

Вы можете найти способ перехода с TestRule на ExternalResource здесь и получить больше информации о тестировании RxJava 2 здесь.