首页 > 解决方案 > 接收流的Android单元测试视图模型

问题描述

我有一个 ViewModel 可以与一个用例对话并返回一个流程,即Flow<MyResult>. 我想对我的 ViewModel 进行单元测试。我是使用流程的新手。需要帮助。这是下面的viewModel -

class MyViewModel(private val handle: SavedStateHandle, private val useCase: MyUseCase) : ViewModel() {

        private val viewState = MyViewState()

        fun onOptionsSelected() =
            useCase.getListOfChocolates(MyAction.GetChocolateList).map {
                when (it) {
                    is MyResult.Loading -> viewState.copy(loading = true)
                    is MyResult.ChocolateList -> viewState.copy(loading = false, data = it.choclateList)
                    is MyResult.Error -> viewState.copy(loading = false, error = "Error")
                }
            }.asLiveData(Dispatchers.Default + viewModelScope.coroutineContext)

MyViewState 看起来像这样 -

 data class MyViewState(
        val loading: Boolean = false,
        val data: List<ChocolateModel> = emptyList(),
        val error: String? = null
    )

单元测试如下所示。断言失败总是不知道我在那里做错了什么。

class MyViewModelTest {

    @get:Rule
    val instantExecutorRule = InstantTaskExecutorRule()

    private val mainThreadSurrogate = newSingleThreadContext("UI thread")

    private lateinit var myViewModel: MyViewModel

    @Mock
    private lateinit var useCase: MyUseCase

    @Mock
    private lateinit var handle: SavedStateHandle

    @Mock
    private lateinit var chocolateList: List<ChocolateModel>

    private lateinit var viewState: MyViewState


    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        Dispatchers.setMain(mainThreadSurrogate)
        viewState = MyViewState()
        myViewModel = MyViewModel(handle, useCase)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
        mainThreadSurrogate.close()
    }

    @Test
    fun onOptionsSelected() {
        runBlocking {
            val flow = flow {
                emit(MyResult.Loading)
                emit(MyResult.ChocolateList(chocolateList))
            }

            Mockito.`when`(useCase.getListOfChocolates(MyAction.GetChocolateList)).thenReturn(flow)
            myViewModel.onOptionsSelected().observeForever {}

            viewState.copy(loading = true)
            assertEquals(viewState.loading, true)

            viewState.copy(loading = false, data = chocolateList)
            assertEquals(viewState.data.isEmpty(), false)
            assertEquals(viewState.loading, true)
        }
    }
}

标签: androidkotlinjunit4kotlin-coroutinesandroid-mvvm

解决方案


此测试环境中存在以下几个问题:

  1. 构建器将flow立即发出结果,因此总是会收到最后一个值。
  2. 持有人与我们的模拟viewState没有联系,因此没用。
  3. 要测试具有多个值的实际流,需要延迟和快进控制。
  4. 需要收集响应值以进行断言

解决方案:

  1. 用于delay处理流程构建器中的两个值
  2. 删除viewState.
  3. 用于MainCoroutineScopeRule控制有延迟的执行流程
  4. 要收集断言的观察者值,请使用ArgumentCaptor.

源代码:

  1. MyViewModelTest.kt

    import androidx.arch.core.executor.testing.InstantTaskExecutorRule
    import androidx.lifecycle.Observer
    import androidx.lifecycle.SavedStateHandle
    import com.pavneet_singh.temp.ui.main.testflow.*
    import org.junit.Assert.assertEquals
    import kotlinx.coroutines.delay
    import kotlinx.coroutines.flow.flow
    import kotlinx.coroutines.runBlocking
    import org.junit.Before
    import org.junit.Rule
    import org.junit.Test
    import org.mockito.ArgumentCaptor
    import org.mockito.Captor
    import org.mockito.Mock
    import org.mockito.Mockito.*
    import org.mockito.MockitoAnnotations
    
    class MyViewModelTest {
    
        @get:Rule
        val instantExecutorRule = InstantTaskExecutorRule()
    
        @get:Rule
        val coroutineScope = MainCoroutineScopeRule()
    
        @Mock
        private lateinit var mockObserver: Observer<MyViewState>
    
        private lateinit var myViewModel: MyViewModel
    
        @Mock
        private lateinit var useCase: MyUseCase
    
        @Mock
        private lateinit var handle: SavedStateHandle
    
        @Mock
        private lateinit var chocolateList: List<ChocolateModel>
    
        private lateinit var viewState: MyViewState
    
        @Captor
        private lateinit var captor: ArgumentCaptor<MyViewState>
    
    
        @Before
        fun setup() {
            MockitoAnnotations.initMocks(this)
            viewState = MyViewState()
            myViewModel = MyViewModel(handle, useCase)
        }
    
        @Test
        fun onOptionsSelected() {
            runBlocking {
                val flow = flow {
                    emit(MyResult.Loading)
                    delay(10)
                    emit(MyResult.ChocolateList(chocolateList))
                }
    
                `when`(useCase.getListOfChocolates(MyAction.GetChocolateList)).thenReturn(flow)
                `when`(chocolateList.get(0)).thenReturn(ChocolateModel("Pavneet", 1))
                val liveData = myViewModel.onOptionsSelected()
                liveData.observeForever(mockObserver)
    
                verify(mockObserver).onChanged(captor.capture())
                assertEquals(true, captor.value.loading)
                coroutineScope.advanceTimeBy(10)
                verify(mockObserver, times(2)).onChanged(captor.capture())
                assertEquals("Pavneet", captor.value.data[0].name)// name is custom implementaiton field of `ChocolateModel` class
            }
        }
    }
    
  2. MainCoroutineScopeRule.kt源复制文件

  3. 清单dependencies

    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
        implementation 'androidx.appcompat:appcompat:1.1.0'
        implementation 'androidx.core:core-ktx:1.2.0'
        implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
        implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
        implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'androidx.test.ext:junit:1.1.1'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
        implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.3.0-alpha01'
        implementation 'org.mockito:mockito-core:2.16.0'
        testImplementation 'androidx.arch.core:core-testing:2.1.0'
        testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.5'
        testImplementation 'org.mockito:mockito-inline:2.13.0'
    }
    

输出(通过删除帧来优化 gif 有点滞后):

流量测试

查看Github 上的 mvvm-flow-coroutine-testing repo 以获得完整的实现。


推荐阅读