【问题标题】:Test LiveData and Coroutines using MockK使用 MockK 测试 LiveData 和协程
【发布时间】:2019-08-30 16:08:38
【问题描述】:

我有这个视图模型:

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。这是测试类:

@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

我该如何解决这个问题?

【问题讨论】:

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


    【解决方案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 提供如此有用的建议!

    【讨论】:

    • 您是否遇到过只运行第一个测试而其余测试失败的问题?我正在尝试使用 mockk 并且我当前的测试类看起来就像你的一样,但是这个问题阻止了我前进。即使我复制了相同的@Test 方法并只是更改了方法名称,它在第一次测试通过后仍然失败。
    • 不,实际上我认为我没有编写任何其他测试。也许尝试为每个测试运行使用不同的调度程序实例。
    【解决方案2】:

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

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

    【讨论】:

    • 感谢您的回答!不幸的是,不可能将viewModelScope 传递给构造函数,因为它是 ViewModel 的扩展属性。
    • 我尝试调用runBlockingTest(Dispatchers.Main),但主调度程序没有实现DelayController。所以你是对的,我需要找到一种方法来更改视图模型使用的调度程序。
    • Dispatcher.setMain(testDispatcher) 可以完成这项工作,但您需要自己在@Before/@After 中管理它并同时使用testDispatcher.runBlockingTest {
    猜你喜欢
    • 2020-11-30
    • 2019-11-23
    • 2023-03-30
    • 1970-01-01
    • 2020-04-12
    • 2020-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多