Нестабильность в тестах на Android с использованием LiveData, RxJava / RxKotlin и Spek

Настраивать:

В нашем проекте (на работе - я не могу опубликовать реальный код) мы реализовали чистый MVVM. Представления взаимодействуют с ViewModels через LiveData. ViewModel содержит два типа сценариев использования: «варианты использования действий» для выполнения каких-либо действий и «варианты использования средства обновления состояния». Обратная связь асинхронна (с точки зрения действия-реакции). Это не похоже на вызов API, в котором вы получаете результат вызова. Это BLE, поэтому после написания характеристики будет характеристика уведомления, которую мы слушаем. Поэтому мы используем много Rx для обновления состояния. Это в Котлине.

ViewModel:

@PerFragment
class SomeViewModel @Inject constructor(private val someActionUseCase: SomeActionUseCase,
                                        someUpdateStateUseCase: SomeUpdateStateUseCase) : ViewModel() {

    private val someState = MutableLiveData<SomeState>()

    private val stateSubscription: Disposable

    // region Lifecycle
    init {
        stateSubscription = someUpdateStateUseCase.state()
                .subscribeIoObserveMain() // extension function
                .subscribe { newState ->
                    someState.value = newState
                })
    }

    override fun onCleared() {
        stateSubscription.dispose()

        super.onCleared()
    }
    // endregion

    // region Public Functions
    fun someState() = someState

    fun someAction(someValue: Boolean) {
        val someNewValue = if (someValue) "This" else "That"

        someActionUseCase.someAction(someNewValue)
    }
    // endregion
}

Вариант использования состояния обновления:

@Singleton
class UpdateSomeStateUseCase @Inject constructor(
            private var state: SomeState = initialState) {

    private val statePublisher: PublishProcessor<SomeState> = 
            PublishProcessor.create()

    fun update(state: SomeState) {
        this.state = state

        statePublisher.onNext(state)
    }

    fun state(): Observable<SomeState> = statePublisher.toObservable()
                                                       .startWith(state)
}

Мы используем Spek для модульных тестов.

@RunWith(JUnitPlatform::class)
class SomeViewModelTest : SubjectSpek<SomeViewModel>({

    setRxSchedulersTrampolineOnMain()

    var mockSomeActionUseCase = mock<SomeActionUseCase>()
    var mockSomeUpdateStateUseCase = mock<SomeUpdateStateUseCase>()

    var liveState = MutableLiveData<SomeState>()

    val initialState = SomeState(initialValue)
    val newState = SomeState(newValue)

    val behaviorSubject = BehaviorSubject.createDefault(initialState)

    subject {
        mockSomeActionUseCase = mock()
        mockSomeUpdateStateUseCase = mock()

        whenever(mockSomeUpdateStateUseCase.state()).thenReturn(behaviorSubject)

        SomeViewModel(mockSomeActionUseCase, mockSomeUpdateStateUseCase).apply {
            liveState = state() as MutableLiveData<SomeState>
        }
    }

    beforeGroup { setTestRxAndLiveData() }
    afterGroup { resetTestRxAndLiveData() }

    context("some screen") {
        given("the action to open the screen") {
            on("screen opened") {
                subject
                behaviorSubject.startWith(initialState)

                it("displays the initial state") {
                    assertEquals(liveState.value, initialState)
                }
            }
        }

        given("some setup") {
            on("some action") {
                it("does something") {
                    subject.doSomething(someValue)

                    verify(mockSomeUpdateStateUseCase).someAction(someOtherValue)
                }
            }

            on("action updating the state") {
                it("displays new state") {
                    behaviorSubject.onNext(newState)

                    assertEquals(liveState.value, newState)
                }
            }
        }
    }
}

Сначала мы использовали Observable вместо BehaviorSubject:

var observable = Observable.just(initialState)
...
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(observable)
...
observable = Observable.just(newState)
assertEquals(liveState.value, newState)

вместо:

val behaviorSubject = BehaviorSubject.createDefault(initialState)
...
whenever(mockSomeUpdateStateUseCase.state()).thenReturn(behaviorSubject)
...
behaviorSubject.onNext(newState)
assertEquals(liveState.value, newState)

но юнит-тест был нестабильным. В основном они проходили (всегда, когда работали изолированно), но иногда они терпели неудачу при запуске всего костюма. Думая, что это связано с асинхронной природой Rx, мы переместили в BehaviourSubject, чтобы иметь возможность контролировать, когда происходит onNext (). Теперь тесты проходят, когда мы запускаем их из AndroidStudio на локальном компьютере, но они все еще нестабильны на машине сборки. Перезапуск сборки часто заставляет их пройти.

Тесты, которые терпят неудачу, - это всегда те, в которых мы утверждаем ценность LiveData. Итак, подозреваемыми являются LiveData, Rx, Spek или их комбинация.

Вопрос: Был ли у кого-нибудь подобный опыт написания модульных тестов с LiveData, с использованием Spek или, может быть, Rx, и нашли ли вы способы их написания, которые решают эти проблемы нестабильности?

....................

Используемые вспомогательные и расширяющие функции:

fun instantTaskExecutorRuleStart() =
        ArchTaskExecutor.getInstance().setDelegate(object : TaskExecutor() {
            override fun executeOnDiskIO(runnable: Runnable) {
                runnable.run()
            }

            override fun isMainThread(): Boolean {
                return true
            }

            override fun postToMainThread(runnable: Runnable) {
                runnable.run()
            }
        })

fun instantTaskExecutorRuleFinish() = ArchTaskExecutor.getInstance().setDelegate(null)

fun setRxSchedulersTrampolineOnMain() = RxAndroidPlugins.setInitMainThreadSchedulerHandler { Schedulers.trampoline() }

fun setTestRxAndLiveData() {
    setRxSchedulersTrampolineOnMain()
    instantTaskExecutorRuleStart()
}

fun resetTestRxAndLiveData() {
    RxAndroidPlugins.reset()
    instantTaskExecutorRuleFinish()
}

fun <T> Observable<T>.subscribeIoObserveMain(): Observable<T> =
        subscribeOnIoThread().observeOnMainThread()

fun <T> Observable<T>.subscribeOnIoThread(): Observable<T> = subscribeOn(Schedulers.io())

fun <T> Observable<T>.observeOnMainThread(): Observable<T> =
        observeOn(AndroidSchedulers.mainThread())
0
0
844
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я не использовал Speck для модульного тестирования. Я использовал платформу модульного тестирования java, и она отлично работает с Rx и LiveData, но вы должны иметь в виду одну вещь. Rx и LiveData являются асинхронными, и вы не можете сделать что-то вроде someObserver.subscribe{}, someObserver.doSmth{}, assert{}, это иногда сработает, но это неправильный способ сделать это.

Для Rx есть TestObservers для наблюдения за событиями Rx. Что-то вроде:

@Test
public void testMethod() {
   TestObserver<SomeObject> observer = new TestObserver()
   someClass.doSomethingThatReturnsObserver().subscribe(observer)
   observer.assertError(...)
   // or
   observer.awaitTerminalEvent(1, TimeUnit.SECONDS)
   observer.assertValue(somethingReturnedForOnNext)
}

Для LiveData вам также придется использовать CountDownLatch, чтобы дождаться выполнения LiveData. Что-то вроде этого:

@Test
public void someLiveDataTest() {
   CountDownLatch latch = new CountDownLatch(1); // if you want to check one time exec
   somethingTahtReturnsLiveData.observeForever(params -> {
      /// you can take the params value here
      latch.countDown();
   }
   //trigger live data here
   ....
   latch.await(1, TimeUnit.SECONDS)
   assert(...)
} 

При таком подходе ваш тест должен работать нормально в любом порядке на любой машине. Кроме того, время ожидания события защелки и терминала должно быть как можно меньше, тесты должны выполняться быстро.

Примечание 1: код находится на JAVA, но вы можете легко изменить его в kotlin.

Примечание 2: Синглтоны - самый большой враг модульного тестирования;). (Со статическими методами на их стороне).

Спасибо @danypata. Попробую защелкнуть в понедельник. Хотя это должно быть что-то порядка миллисекунд, поскольку существует много тестов, использующих живые данные, поэтому что-то большее добавило бы слишком много времени для выполнения. Во втором примечании, варианты использования обновления должны быть одиночными, поскольку наблюдающий класс должен наблюдать то же самое, что и обновляется. Если бы у каждого был свой экземпляр, это не сработало бы. Однако это не имеет никакого значения для этих тестов.

Vlad 12.10.2018 12:16

Одна вещь, которую я забыл упомянуть, время ожидания - это МАКСИМАЛЬНОЕ время ожидания защелки или тестового наблюдателя, если событие запускается быстрее, время ожидания будет проигнорировано. Таким образом, 1 секунда - это не фактическое время ожидания, а максимальное время ожидания.

danypata 12.10.2018 12:18

Конечно, это хорошо.

Vlad 12.10.2018 12:21

Вам даже не нужен TestObserver, проверьте этот reactivex.io/RxJava/1.x/javadoc/rx/Observable.html#test--, он предоставит вам свободный API.

nikis 12.10.2018 13:32
Ответ принят как подходящий

Проблема не в LiveData; это более распространенная проблема - одиночки. Здесь Update...StateUseCases должен был быть синглтоном; в противном случае, если бы наблюдатели получили другой экземпляр, у них был бы другой PublishProcessor, и они не получили бы то, что было опубликовано.

Существует тест для каждого Update...StateUseCases и тест для каждой ViewModel, в которую вводится Update...StateUseCases (хорошо косвенно через ...StateObserver).

Состояние существует в Update...StateUseCases, и, поскольку это синглтон, оно изменяется в обоих тестах, и они используют один и тот же экземпляр, становясь зависимыми друг от друга.

Во-первых, по возможности старайтесь избегать использования синглтонов.

Если нет, сбрасывайте состояние после каждой тестовой группы.

Другие вопросы по теме