Android 接收流的单元测试视图模型
Android unit testing view model that receives flow
我有一个 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
持有者没有 link 我们的模拟因此是无用的。
- 多值测试实际流量,需要延迟和快进控制。
- 需要收集响应值以进行断言
解决方案:
- 使用
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 回购以获得完整的实施。
我想我找到了一种更好的测试方法,即使用 Channel 和 consumeAsFlow
扩展函数。至少在我的测试中,我似乎能够测试通过通道发送的多个值(作为流量使用)。
所以..假设您有一些公开 Flow<String>
的用例组件。在您的 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"))
}
编辑:我想您也可以使用任何类型的 hot 流程实现来代替 Channel
和 consumeAsFlow
。例如,您可以使用 MutableSharedFlow
使您能够在需要时 emit
值。
我有一个 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
持有者没有 link 我们的模拟因此是无用的。- 多值测试实际流量,需要延迟和快进控制。
- 需要收集响应值以进行断言
解决方案:
- 使用
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 回购以获得完整的实施。
我想我找到了一种更好的测试方法,即使用 Channel 和 consumeAsFlow
扩展函数。至少在我的测试中,我似乎能够测试通过通道发送的多个值(作为流量使用)。
所以..假设您有一些公开 Flow<String>
的用例组件。在您的 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"))
}
编辑:我想您也可以使用任何类型的 hot 流程实现来代替 Channel
和 consumeAsFlow
。例如,您可以使用 MutableSharedFlow
使您能够在需要时 emit
值。