0

設定:

私たちのプロジェクト (作業中 - 実際のコードを投稿することはできません) では、クリーンな MVVM を実装しました。ビューは、LiveData を介して ViewModel と通信します。ViewModel は、何かを行うための「アクション ユース ケース」と「ステート アップデーター ユース ケース」の 2 種類のユース ケースをホストします。後方通信は非同期です (アクションの反応に関して)。呼び出しから結果を取得する 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)
                }
            }
        }
    }
}

最初は、BehaviorSubject の代わりに Observable を使用していました。

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 の非同期性に関係していると考えて、onNext() がいつ発生するかを制御できるように BehaviourSubject に移動しました。ローカル マシンで 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())
4

2 に答える 2