使用 MockK 测试 LiveData 和协程

Test LiveData and Coroutines using 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
        }
    }
}

我想编写一个测试来检查状态是否真的是 LOADINGmyUseCase.loadStuff() 是 运行。我正在为此使用 MockK。这是测试 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)
    }
}

失败:

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

我该如何解决这个问题?

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

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

我只需要在测试中做一些更改 class 就可以通过:

@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 提供如此有用的建议!