【问题标题】:Flakiness in tests on Android using LiveData, RxJava/RxKotlin and Spek使用 LiveData、RxJava/RxKotlin 和 Spek 在 Android 上进行测试的脆弱性
【发布时间】:2018-10-12 09:46:14
【问题描述】:

设置:

在我们的项目中(在工作中 - 我无法发布真实代码),我们实现了干净的 MVVM。视图通过 LiveData 与 ViewModel 通信。 ViewModel 承载两种用例:做某事的“动作用例”和“状态更新用例”。反向通信是异步的(就动作反应而言)。它不像 API 调用,您可以从调用中获取结果。是BLE,所以写完特性后会有一个我们监听的通知特性。所以我们使用了大量的 Rx 来更新状态。它在 Kotlin 中。

视图模型:

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

【问题讨论】:

    标签: android unit-testing rx-java android-livedata spek


    【解决方案1】:

    我没有使用 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。我会在星期一尝试闩锁。虽然它必须是毫秒级的,因为有许多测试使用实时数据,所以任何更高的时间都会增加执行时间。第二点,更新用例必须是单例的,因为类观察需要观察被更新的同一事物。如果每个人都有自己的实例,它就行不通了。但这并不是说它对这些测试有任何影响。
    • 有一件事我忘了提,等待时间是闩锁或测试观察者等待的最大时间,如果事件触发得更快,等待时间将被忽略。所以 1 秒不是实际等待时间,而是 MAX 等待时间。
    • 当然,那很好。
    • 你甚至不需要TestObserver,检查这个reactivex.io/RxJava/1.x/javadoc/rx/Observable.html#test--,它会给你流畅的API。
    【解决方案2】:

    问题不在于LiveData;这是更常见的问题 - 单身人士。这里 Update...StateUseCases 必须是单例;否则,如果观察者获得不同的实例,他们将拥有不同的 PublishProcessor 并且不会获得发布的内容。

    每个Update...StateUseCases 都有一个测试,每个注入Update...StateUseCases 的ViewModel 都有一个测试(通过...StateObserver 间接地)。

    状态存在于Update...StateUseCases 中,因为它是一个单例,所以它在两个测试中都会发生变化,并且它们使用相同的实例变得相互依赖。

    如果可能,首先尽量避免使用单例。

    如果不是,则在每个测试组之后重置状态。

    【讨论】:

      猜你喜欢
      • 2021-06-16
      • 1970-01-01
      • 1970-01-01
      • 2022-10-22
      • 2010-10-02
      • 2017-09-19
      • 1970-01-01
      • 2014-10-21
      • 1970-01-01
      相关资源
      最近更新 更多