使用MockK测试LiveData和协程

时间:2019-08-30 16:08:38

标签: android testing android-livedata kotlin-coroutines mockk

我有这个视图模型:

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
        }
    }
}

我想编写一个测试,以检查LOADING运行时状态是否真的为myUseCase.loadStuff()。我正在使用MockK。这是测试课程:

@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)
    }
}

失败:

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

我该如何解决?

2 个答案:

答案 0 :(得分:1)

您的问题在于viewModelScope分发给Dispatcher.MAIN,而不是runBlockingTest创建的测试分发者。这意味着即使调用advanceTimeBy,代码也不会执行。

您可以通过使用Dispatcher.setMain(..)用测试调度程序替换MAIN调度程序来解决此问题。这将需要自己管理调度程序,而不是依靠独立的runBlockingTest

答案 1 :(得分:1)

我只需要在测试类中进行一些更改即可使其通过:

@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)
        }
    }
}

完全不需要在视图模型中进行任何更改! :D

感谢Kiskae提供有用的建议!