Warning: file_get_contents(/data/phpspider/zhask/data//catemap/4/kotlin/3.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
接收流的Android单元测试视图模型_Android_Kotlin_Junit4_Kotlin Coroutines_Android Mvvm - Fatal编程技术网

接收流的Android单元测试视图模型

接收流的Android单元测试视图模型,android,kotlin,junit4,kotlin-coroutines,android-mvvm,Android,Kotlin,Junit4,Kotlin Coroutines,Android Mvvm,我有一个ViewModel,它与用例对话并得到一个流,即flow。我想对我的ViewModel进行单元测试。我不熟悉使用流。需要帮助请。以下是viewModel- class MyViewModel(private val handle: SavedStateHandle, private val useCase: MyUseCase) : ViewModel() { private val viewState = MyViewState() fun onOp

我有一个ViewModel,它与用例对话并得到一个流,即
flow
。我想对我的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
    )
数据类MyViewState(
val加载:布尔值=false,
val数据:List=emptyList(),
val错误:字符串?=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)
        }
    }
}
类MyViewModelTest{
@获取:规则
val instantExecutorRule=instantetaskexecutorrule()
private val mainthreadsprogate=newSingleThreadContext(“UI线程”)
私有lateinit var myViewModel:myViewModel
@嘲弄
私有lateinit变量用例:MyUseCase
@嘲弄
私有lateinit变量句柄:SavedStateHandle
@嘲弄
私有lateinit变量巧克力列表:列表
私有lateinit变量viewState:MyViewState
@以前
趣味设置(){
initMocks(this)
Dispatchers.setMain(mainthreadsubrogate)
viewState=MyViewState()
myViewModel=myViewModel(句柄,用例)
}
@之后
有趣的撕裂{
Dispatchers.resetMain()//将主调度器重置为原始主调度器
mainthreadsrogate.close()
}
@试验
趣味onoptionselected(){
运行阻塞{
val流量=流量{
发射(MyResult.Loading)
发出(MyResult.ChocolateList(ChocolateList))
}
Mockito.`when`(useCase.getListOfChocolates(MyAction.getChocolates))。然后返回(flow)
myViewModel.OnOptionSelected().ObserveForRever{}
viewState.copy(加载=true)
assertEquals(viewState.loading,true)
copy(加载=假,数据=巧克力列表)
assertEquals(viewState.data.isEmpty(),false)
assertEquals(viewState.loading,true)
}
}
}

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

  • 生成器将立即发出结果,因此始终会收到最后一个值
  • viewState
    持有者与我们的mock没有链接,因此是无用的
  • 为了测试具有多个值的实际流量,需要延迟和快进控制
  • 需要为断言收集响应值
  • 解决方案:

  • 使用
    delay
    在流生成器中处理这两个值
  • 删除
    viewState
  • 使用
    MainCoroutineScopeRule
    控制延迟执行流
  • 要收集用于断言的观察者值,请使用
    ArgumentCaptor
  • 源代码:

  • 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
            }
        }
    }
    
  • 输出(gif通过删除帧进行优化,因此位延迟):


    查看Github上的repo以完成实现

    我想我已经找到了一个更好的方法来测试这一点,通过使用Channel和
    consumerasflow
    扩展函数。至少在我的测试中,我似乎能够测试通过通道发送的多个值(作为流使用)

    所以。。假设您有一些用例组件公开了一个
    。在
    ViewModelTest
    中,您希望检查每次发出值时,UI状态是否会更新为某个值。 在我的例子中,UI状态是一个
    StateFlow
    ,但这也应该可以通过LiveData实现。 此外,我正在使用MockK,但使用Mockito也应该很容易

    鉴于此,以下是我的测试结果:

    @Test
    fun test() = runBlocking(testDispatcher) {
    
        val channel = Channel<String>()
        every { mockedUseCase.someDataFlow } returns channel.consumeAsFlow()
    
        channel.send("a")
        assertThat(viewModelUnderTest.uiState.value, `is`("a"))
    
        channel.send("b")
        assertThat(viewModelUnderTest.uiState.value, `is`("b"))
    }
    
    @测试
    fun test()=运行阻塞(testDispatcher){
    val通道=通道()
    每个{mockedUseCase.someDataFlow}返回channel.consumerasflow()
    频道发送(“a”)
    断言(viewModelUnderTest.uiState.value,`is`(“a”))
    频道发送(“b”)
    断言(viewModelUnderTest.uiState.value,`is`(“b”))
    }
    

    编辑:我想您也可以使用任何类型的热流实现,而不是
    Channel
    consumerasflow
    。例如,您可以使用
    MutableSharedFlow
    ,它允许您在需要时发出
    值。

    您需要在此处测试什么?需要测试的确切场景是什么?从用文字描述测试开始。@Code peedient-我需要测试viewmodel是否从用例接收到流,并在其上构建视图状态并将其提供给observer@Maria哪种方法有效?onOptions Selected()在viewmodel@IgorGanapolsky我的片段观察到一个livedata,因此我需要在ViewModel中为流做一个livedata我们如何以这种方式测试livedata?@IgorGanapolsky有多种方法,如果你有一个模拟或伪造的存储库,那么你可以用所需的数据触发事件。或者您可以使用livedatabuilder或@Pavneet_Singh如果收到的数据是
    PagingData
    ?@Dr.jacky根据您的实现和流程,您可能需要设置模拟对象或面。“src/test/”和“src/androidTest”文件夹之间的区别是什么?它是如何在主线程中处理viewmodel函数的执行的?
    @Test
    fun test() = runBlocking(testDispatcher) {
    
        val channel = Channel<String>()
        every { mockedUseCase.someDataFlow } returns channel.consumeAsFlow()
    
        channel.send("a")
        assertThat(viewModelUnderTest.uiState.value, `is`("a"))
    
        channel.send("b")
        assertThat(viewModelUnderTest.uiState.value, `is`("b"))
    }