简体   繁体   English

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

[英]Android unit testing view model that receives flow

I have a ViewModel that talks to a use case and gets a flow back ie Flow<MyResult> .我有一个 ViewModel 与用例对话并返回一个流,即Flow<MyResult> I want to unit test my ViewModel.我想对我的 ViewModel 进行单元测试。 I am new to using the flow.我是使用流程的新手。 Need help pls.需要帮助请。 Here is the viewModel below -这是下面的视图模型 -

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 looks like this - MyViewState 看起来像这样 -

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

The unit test looks like below.单元测试如下所示。 The assert fails always don't know what I am doing wrong there.断言失败总是不知道我在那里做错了什么。

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

There are few issues in this testing environment as:此测试环境中存在的问题很少:

  1. The flow builder will emit the result instantly so always the last value will be received. flow器将立即发出结果,因此总是会收到最后一个值。
  2. The viewState holder has no link with our mocks hence is useless. viewState持有者与我们的viewState没有联系,因此是无用的。
  3. To test the actual flow with multiple values, delay and fast-forward control is required.要测试具有多个值的实际流量,需要延迟和快进控制。
  4. The response values need to be collected for assertion需要收集响应值以进行断言

Solution:解决方案:

  1. Use delay to process both values in the flow builder使用delay处理流构建器中的两个值
  2. Remove viewState .删除viewState
  3. Use MainCoroutineScopeRule to control the execution flow with delay使用MainCoroutineScopeRule控制延迟执行流程
  4. To collect observer values for assertion, use ArgumentCaptor .要收集断言的观察者值,请使用ArgumentCaptor

Source-code:源代码:

  1. MyViewModelTest.kt 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 source to copy the file MainCoroutineScopeRule.kt源文件复制

  3. List of dependencies 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' }

Output (gif is optimized by removing frames so bit laggy):输出(通过删除一些延迟的帧来优化 gif):

流量测试

View mvvm-flow-coroutine-testing repo on Github for complete implementaion.在 Github 上查看mvvm-flow-coroutine-testing repo 以获得完整的实现。

I think I have found a better way to test this, by using Channel and consumeAsFlow extension function.我想我找到了一个更好的方法来测试这个,通过使用 Channel 和consumeAsFlow扩展功能。 At least in my tests, I seem to be able to test multiple values sent throught the channel (consumed as flow).至少在我的测试中,我似乎能够测试通过通道发送的多个值(作为流使用)。

So.. say you have some use case component that exposes a Flow<String> .所以.. 假设你有一些暴露Flow<String>用例组件。 In your ViewModelTest , you want to check that everytime a value is emitted, the UI state gets updated to some value.在您的ViewModelTest ,您想检查每次发出一个值时,UI 状态是否更新为某个值。 In my case, UI state is a StateFlow , but this should be do-able with LiveData as well.就我而言,UI 状态是一个StateFlow ,但这也应该可以与 LiveData 一起使用。 Also, I am using MockK, but should also be easy with Mockito.另外,我正在使用 MockK,但使用 Mockito 也应该很容易。

Given this, here is how my test looks:鉴于此,这是我的测试的样子:

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

EDIT: I guess you can also use any kind of hot flow implementation instead of Channel and consumeAsFlow .编辑:我想你也可以使用任何类型的流量实现的,而不是ChannelconsumeAsFlow For example, you can use a MutableSharedFlow that enables you to emit values when you want.例如,您可以使用MutableSharedFlow使您能够在需要时emit值。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM