使用 LiveData、RxJava/RxKotlin 和 Spek 在 Android 上进行的测试中出现不稳定
Flakiness in tests on Android using LiveData, RxJava/RxKotlin and Spek
设置:
在我们的项目中(在工作中 - 我无法 post 真正的代码),我们实现了干净的 MVVM。视图通过 LiveData 与 ViewModel 通信。 ViewModel 承载两种用例:'action use cases' 做某事,和 'state updater use cases'。反向通信是异步的(就动作反应而言)。它不像 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())
我没有使用 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:单例是单元测试的最大敌人;)。 (他们身边有静态方法)。
问题不在于 LiveData
;这是更常见的问题 - 单身人士。这里的 Update...StateUseCases
必须是单例;否则,如果观察者获得不同的实例,他们将拥有不同的 PublishProcessor,并且不会获得已发布的内容。
每个 Update...StateUseCases
都有一个测试,每个 Update...StateUseCases
注入的 ViewModel 都有一个测试(通过 ...StateObserver
间接地)。
状态存在于 Update...StateUseCases
中,由于它是单例,因此在两个测试中都会发生变化,并且它们使用相同的实例变得相互依赖。
首先尽可能避免使用单例。
如果不是,则在每个测试组后重置状态。
设置:
在我们的项目中(在工作中 - 我无法 post 真正的代码),我们实现了干净的 MVVM。视图通过 LiveData 与 ViewModel 通信。 ViewModel 承载两种用例:'action use cases' 做某事,和 'state updater use cases'。反向通信是异步的(就动作反应而言)。它不像 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())
我没有使用 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:单例是单元测试的最大敌人;)。 (他们身边有静态方法)。
问题不在于 LiveData
;这是更常见的问题 - 单身人士。这里的 Update...StateUseCases
必须是单例;否则,如果观察者获得不同的实例,他们将拥有不同的 PublishProcessor,并且不会获得已发布的内容。
每个 Update...StateUseCases
都有一个测试,每个 Update...StateUseCases
注入的 ViewModel 都有一个测试(通过 ...StateObserver
间接地)。
状态存在于 Update...StateUseCases
中,由于它是单例,因此在两个测试中都会发生变化,并且它们使用相同的实例变得相互依赖。
首先尽可能避免使用单例。
如果不是,则在每个测试组后重置状态。