Слабость в тестах на Android с использованием LiveData, RxJava / RxKotlin и Spek - PullRequest
0 голосов
/ 12 октября 2018

Настройка:

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

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

Ответы [ 2 ]

0 голосов
/ 19 ноября 2018

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

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

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

Сначала попытайтесь по возможности избегать использования синглетонов.

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

0 голосов
/ 12 октября 2018

Я не использовал 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: Синглтон - самый большой враг юнит-тестирования;).(Со статическими методами на их стороне).

...