RxJava 2 переопределяет планировщик ввода-вывода в модульном тесте

Я пытаюсь протестировать следующий код 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()
})

person Alex    schedule 07.04.2017    source источник
comment
См. обновленный документ Javadoc для версии 2.0.8: reactivex.io/RxJava/2.x/javadoc/io/reactivex/schedulers/   -  person akarnokd    schedule 07.04.2017
comment
В частности, обратите внимание, что из-за возможных циклов инициализации использование любого из других методов, возвращающих планировщик, приведет к исключению NullPointerException.   -  person Kiskae    schedule 07.04.2017
comment
После инициализации класса Schedulers вы можете переопределить возвращенный экземпляр Scheduler с помощью метода RxJavaPlugins.setIoSchedulerHandler(io.reactivex.functions.Function).   -  person akarnokd    schedule 07.04.2017
comment
Спасибо! Раньше я пытался использовать setIoSchedulerHandler, но flatMap не вызывался. Наконец-то понял, почему: метод validate возвращал наблюдаемую, которая делала emitter.onNext(null) :/ Поскольку в RxJava 2 больше не принимаются значения NULL, я изменил это значение на Completable, и теперь тесты проходят!   -  person Alex    schedule 07.04.2017


Ответы (4)


Я предлагаю вам использовать другой подход и добавить уровень абстракции к вашим планировщикам. У этого парня есть хорошая статья об этом.

В Котлине это выглядело бы примерно так

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

person solidak    schedule 10.04.2017
comment
Спасибо, что поделились батутом. Я использовал немедленное, но его больше нет в Rx2. - person Leo Droidcoder; 19.09.2017
comment
Мне не нужен этот уровень абстракции. Такое ощущение, что мы значительно модифицируем производственный код только ради тестирования, когда у нас уже есть средства для его тестирования без такой абстракции. Ответ, предоставленный @Alex, отлично работает. - person Henry; 01.04.2018
comment
Я должен был бы согласиться с @Henry здесь. Намного практичнее просто модифицировать планировщики во время тестирования с помощью RxJavaPlugin..., чем изменять производственный код только ради тестов. - person William Reed; 07.01.2019

Догадаться! Это было связано с тем, что в этом коде:

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() }

Теперь испытания пройдены!

person Alex    schedule 07.04.2017

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

RxJavaPlugins.setIoSchedulerHandler(scheduler -> Schedulers.trampoline())
person Efi G    schedule 27.11.2018

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

@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 здесь.

person Sebastian    schedule 28.06.2019