android - 接收流的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)
}
}
}
解决方案
此测试环境中存在以下几个问题:
- 构建器将
flow
立即发出结果,因此总是会收到最后一个值。 - 持有人与我们的模拟
viewState
没有联系,因此没用。 - 要测试具有多个值的实际流,需要延迟和快进控制。
- 需要收集响应值以进行断言
解决方案:
- 用于
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 } } }
清单
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 以获得完整的实现。
推荐阅读
- sql-server - Crystal Report 未显示来自 SQL Server 17.5 的数据
- python - pyppeteer.errors.BrowserError:无法连接到浏览器端口
- python - 交错4个相同长度的python列表
- selenium - Selenium-Firefox 驱动程序问题(Firefox 驱动程序无法解析为类型
- camera - Raspberry Pi Zero W 在启动时启动脚本
- apache-spark - dataproc 如何与谷歌云存储配合使用?
- java - Android - java将全局上下文设置为另一个类
- macos - 无法安装 VisualSFM macOS High Sierra,因为它需要不再支持的 cairo-x11
- c# - C# 使用取消令牌异步发送多封电子邮件
- javascript - 反应道具:无法访问数组中对象中的键,索引都通过道具传递