使用LiveData,RxJava / RxKotlin和Spek在Android上的测试中的不稳定



Setup:

在我们的项目中(在工作中 - 我无法发布真正的代码(,我们已经实现了干净的 MVVM。视图通过 LiveData 与 ViewModels 通信。ViewModel 托管两种用例:"操作用例"来执行某些操作,以及"状态更新程序用例"。反向通信是异步的(就动作反应而言(。它不像 API 调用,您可以在其中从调用中获取结果。它是 BLE,所以在编写特征之后,我们会听到一个通知特征。所以我们使用了很多 Rx 来更新状态。它在科特林。

视图模型:

@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 的异步性质有关,我们转移到 BehaviorSubject 以便能够控制 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())

我没有使用Speck进行单元测试。我使用过java单元测试平台,它与Rx和LiveData完美配合,但你必须记住一件事。Rx和LiveData是异步的,你不能做类似的事情someObserver.subscribe{}, someObserver.doSmth{}, assert{}这有时会起作用,但这不是正确的方法。

对于 Rx,有观察 Rx 事件的TestObservers。像这样:

@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:单例是单元测试;)的最大敌人。(使用静态方法(。

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

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

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

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

如果没有,请在每个测试组之后重置状态。

最新更新