Unit test the new Kotlin coroutine StateFlow

懵懂的女人 提交于 2020-12-05 06:55:49

问题


Recently was introduced the class StateFlow as part of Kotlin coroutine.

I'm currently trying it and encounter an issue while trying to unit test my ViewModel. What I want to achieve: testing that my StateFlow is receiving all the state values in the correct order in my ViewModel.

My code is as follow:

ViewModel:

class WalletViewModel(private val getUserWallets: GetUersWallets) : ViewModel() {

val userWallet: StateFlow<State<UserWallets>> get() = _userWallets
private val _userWallets: MutableStateFlow<State<UserWallets>> =
    MutableStateFlow(State.Init)

fun getUserWallets() {
    viewModelScope.launch {
        getUserWallets.getUserWallets()
            .onStart { _userWallets.value = State.Loading }
            .collect { _userWallets.value = it }
    }
}

My test:

    @Test
fun `observe user wallets ok`() = runBlockingTest {
    Mockito.`when`(api.getAssetWallets()).thenReturn(TestUtils.getAssetsWalletResponseOk())
    Mockito.`when`(api.getFiatWallets()).thenReturn(TestUtils.getFiatWalletResponseOk())

    viewModel.getUserWallets()

    val res = arrayListOf<State<UserWallets>>()
    viewModel.userWallet.toList(res) //doesn't works

    Assertions.assertThat(viewModel.userWallet.value is State.Success).isTrue() //works, last value enmited
}

Accessing the last value emitted works. But What I want to test is that all the emitted values are emitted in the correct order. with this piece of code: viewModel.userWallet.toList(res) //doesn't works I'm getting the following error :

java.lang.IllegalStateException: This job has not completed yet
    at kotlinx.coroutines.JobSupport.getCompletionExceptionOrNull(JobSupport.kt:1189)
    at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:53)
    at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest$default(TestBuilders.kt:45)
    at WalletViewModelTest.observe user wallets ok(WalletViewModelTest.kt:52)
....

I guess I'm missing something obvious. But not sure why as I'm just getting started with Coroutine and Flow and this error seems to happen when not using runBlockingTest, which I use already.

EDIT: As a temporary solution, I'm testing it as a live data:

    @Captor
    lateinit var captor: ArgumentCaptor<State<UserWallets>>

    @Mock
    lateinit var walletsObserver: Observer<State<UserWallets>>

    @Test
    fun `observe user wallets ok`() = runBlockingTest {
        viewModel.userWallet.asLiveData().observeForever(walletsObserver)

        viewModel.getUserWallets()

        captor.run {
            Mockito.verify(walletsObserver, Mockito.times(3)).onChanged(capture())
            Assertions.assertThat(allValues[0] is State.Init).isTrue()
            Assertions.assertThat(allValues[1] is State.Loading).isTrue()
            Assertions.assertThat(allValues[2] is State.Success).isTrue()
        }
    }

回答1:


runBlockingTest just skips the delays in your case but not override the dispatcher used in the ViewModel with your test dispatcher. You need to inject TestCoroutineDispatcher to your ViewModel or since you are using viewModelScope.launch {} which already uses Dispatchers.Main by default, you need to override the main dispatcher via Dispatchers.setMain(testCoroutineDispatcher). You can create and add the following rule to your test file.

class MainCoroutineRule(
        val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
} 

And in your test file

@get:Rule
var mainCoroutineRule = MainCoroutineRule()

@Test
fun `observe user wallets ok`() = mainCoroutineRule.testDispatcher.runBlockingTest {
}

Btw it is always a good practice to inject dispatchers. For instance if you would have been using a dispatcher other than Dispatchers.Main in your coroutine scope like viewModelScope.launch(Dispatchers.Default), then your test will fail again even if you are using a test dispatcher. The reason is you can only override main dispatcher with Dispatchers.setMain() as it can be understood from its name but not Dispatchers.IO or Dispatchers.Default. In that case you need to inject mainCoroutineRule.testDispatcher to your view model and use the injected dispatcher rather than hardcoding it.



来源:https://stackoverflow.com/questions/62110761/unit-test-the-new-kotlin-coroutine-stateflow

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!