Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Test LiveData and Coroutines using MockK

I have this view model:

class MyViewModel(private val myUseCase: MyUseCase) : ViewModel() {

    val stateLiveData = MutableLiveData(State.IDLE)

    fun onButtonPressed() {
        viewModelScope.launch {
            stateLiveData.value = State.LOADING
            myUseCase.loadStuff() // Suspend
            stateLiveData.value = State.SUCCESS
        }
    }
}

I'd like to write a test that checks whether the state is really LOADING while myUseCase.loadStuff() is running. I'm using MockK for that. Here's the test class:

@ExperimentalCoroutinesApi
class MyViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    private lateinit var myUseCase: MyUseCase
    private lateinit var myViewModel: MyViewModel

    @Before
    fun setup() {
        myUseCase = mockkClass(MyUseCase::class)
        myViewModel = MyViewModel(myUseCase)
    }

    @Test
    fun `button click should put screen into loading state`() = runBlockingTest {
        coEvery { myUseCase.loadStuff() } coAnswers  { delay(2000) }
        myViewModel.onButtonPressed()
        advanceTimeBy(1000)
        val state = myViewModel.stateLiveData.value
        assertEquals(State.LOADING, state)
    }
}

It fails:

java.lang.AssertionError: 
Expected :LOADING
Actual   :IDLE

How can I fix this?

like image 305
Milack27 Avatar asked Aug 30 '19 16:08

Milack27


2 Answers

I only needed to make a few changes in the test class to make it pass:

@ExperimentalCoroutinesApi
class MyViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    private val dispatcher = TestCoroutineDispatcher()

    private lateinit var myUseCase: MyUseCase
    private lateinit var myViewModel: MyViewModel

    @Before
    fun setup() {
        Dispatchers.setMain(dispatcher)

        myUseCase = mockkClass(MyUseCase::class)
        myViewModel = MyViewModel(myUseCase)
    }

    @After
    fun cleanup() {
        Dispatchers.resetMain()
    }

    @Test
    fun `button click should put screen into loading state`() {
        dispatcher.runBlockingTest {
            coEvery { myUseCase.loadStuff() } coAnswers  { delay(2000) }
            myViewModel.onButtonPressed()

            // This isn't even needed.
            advanceTimeBy(1000)

            val state = myViewModel.stateLiveData.value
            assertEquals(State.LOADING, state)
        }
    }
}

No changes needed in the view model at all! :D

Thanks Kiskae for such helpful advice!

like image 53
Milack27 Avatar answered Oct 20 '22 13:10

Milack27


Your problem lies in the fact that viewModelScope dispatches to Dispatcher.MAIN, not the testing dispatcher created by runBlockingTest. This means that even with the call to advanceTimeBy the code does not get executed.

You can solve the issue by using Dispatcher.setMain(..) to replace the MAIN dispatcher with your test dispatcher. This will require managing the dispatcher yourself instead of relying on the stand-alone runBlockingTest.

like image 24
Kiskae Avatar answered Oct 20 '22 13:10

Kiskae