设置:
在我们的项目中(正在工作-我无法发布真实代码),我们实现了干净的MVVM。视图通过LiveData与ViewModels通信。 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或其组合。
问题:是否有人有类似的经验,使用Spek或Rx使用LiveData编写单元测试,您是否找到了编写解决这些脆弱性问题的方法?
....................
使用的辅助功能和扩展功能:
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)
我没有使用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:Singleton是单元测试的最大敌人;)。 (旁边有静态方法)。
答案 1 :(得分:0)
与LiveData
无关的问题;这是更常见的问题-单身人士。 Update...StateUseCases
在这里必须是单例。否则,如果观察者获得不同的实例,则他们将具有不同的PublishProcessor,而不会获得所发布的内容。
每个Update...StateUseCases
都有一个测试,每个注入Update...StateUseCases
的ViewModel都有一个测试(通过...StateObserver
间接进行)。
状态存在于Update...StateUseCases
中,由于它是一个单例,因此在两个测试中都会更改,并且它们使用同一实例彼此依赖。
如果可能,首先尝试避免使用单例。
如果没有,请在每个测试组之后重置状态。