使用 StateFlow 和协程的单元测试 viewModel
Kotlin 1.4.21
我有一个非常简单的 ViewModel,它使用协程和 stateFlow。但是,单元测试将失败,因为 stateFlow 似乎没有更新。
我认为这是因为测试将在 stateFlow
更新之前完成。
预计不为空
这是我正在测试的ViewModel
class TrendingSearchViewModel @Inject constructor(
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase,
private val coroutineDispatcher: CoroutineDispatcherProvider
) : ViewModel() {
private val trendingSearchMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
val trendingSearchStateFlow = trendingSearchMutableStateFlow.asStateFlow()
fun getTrendingSearch() {
viewModelScope.launch(coroutineDispatcher.io()) {
try {
trendingSearchMutableStateFlow.value = loadTrendingSearchUseCase.execute()
} catch (exception: Exception) {
Timber.e(exception, "trending ${exception.localizedMessage}")
}
}
}
}
这是我的实际测试类,我尝试了不同的方法来让它工作
class TrendingSearchViewModelTest {
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase = mock()
private val coroutineDispatcherProvider = CoroutineDispatcherProviderImp()
private lateinit var trendingSearchViewModel: TrendingSearchViewModel
@Before
fun setUp() {
trendingSearchViewModel = TrendingSearchViewModel(
loadTrendingSearchUseCase,
coroutineDispatcherProvider
)
}
@Test
fun `should get trending search suggestions`() {
runBlocking {
// Arrange
val trending1 = UUID.randomUUID().toString()
val trending2 = UUID.randomUUID().toString()
val trending3 = UUID.randomUUID().toString()
whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOf(trending1, trending2, trending3))
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.value
}
// Act
trendingSearchViewModel.getTrendingSearch()
// Assert
val result = trendingSearchViewModel.trendingSearchStateFlow.value
assertThat(result).isNotEmpty()
job.cancel()
}
}
}
这是我在测试中嘲笑的用例:
class LoadTrendingSearchUseCaseImp @Inject constructor(
private val searchCriteriaProvider: SearchCriteriaProvider,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : LoadTrendingSearchUseCase {
override suspend fun execute(): List<String> {
return withContext(coroutineDispatcherProvider.io()) {
searchCriteriaProvider.provideTrendingSearch().trendingSearches
}
}
}
以防万一需要这是我的界面:
interface CoroutineDispatcherProvider {
fun io(): CoroutineDispatcher = Dispatchers.IO
fun default(): CoroutineDispatcher = Dispatchers.Default
fun main(): CoroutineDispatcher = Dispatchers.Main
fun immediate(): CoroutineDispatcher = Dispatchers.Main.immediate
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}
class CoroutineDispatcherProviderImp @Inject constructor() : CoroutineDispatcherProvider
Kotlin 1.4.21
I have a very simple ViewModel that uses coroutine and stateFlow. However, the unit test will fail as the stateFlow doesn't seem to get updated.
I think its because the test will finish before the stateFlow
is updated.
expected not to be empty
This is my ViewModel under test
class TrendingSearchViewModel @Inject constructor(
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase,
private val coroutineDispatcher: CoroutineDispatcherProvider
) : ViewModel() {
private val trendingSearchMutableStateFlow = MutableStateFlow<List<String>>(emptyList())
val trendingSearchStateFlow = trendingSearchMutableStateFlow.asStateFlow()
fun getTrendingSearch() {
viewModelScope.launch(coroutineDispatcher.io()) {
try {
trendingSearchMutableStateFlow.value = loadTrendingSearchUseCase.execute()
} catch (exception: Exception) {
Timber.e(exception, "trending ${exception.localizedMessage}")
}
}
}
}
This is my actual test class, I have tried different things to get it to work
class TrendingSearchViewModelTest {
private val loadTrendingSearchUseCase: LoadTrendingSearchUseCase = mock()
private val coroutineDispatcherProvider = CoroutineDispatcherProviderImp()
private lateinit var trendingSearchViewModel: TrendingSearchViewModel
@Before
fun setUp() {
trendingSearchViewModel = TrendingSearchViewModel(
loadTrendingSearchUseCase,
coroutineDispatcherProvider
)
}
@Test
fun `should get trending search suggestions`() {
runBlocking {
// Arrange
val trending1 = UUID.randomUUID().toString()
val trending2 = UUID.randomUUID().toString()
val trending3 = UUID.randomUUID().toString()
whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOf(trending1, trending2, trending3))
val job = launch {
trendingSearchViewModel.trendingSearchStateFlow.value
}
// Act
trendingSearchViewModel.getTrendingSearch()
// Assert
val result = trendingSearchViewModel.trendingSearchStateFlow.value
assertThat(result).isNotEmpty()
job.cancel()
}
}
}
This is the usecase I am mocking in the test:
class LoadTrendingSearchUseCaseImp @Inject constructor(
private val searchCriteriaProvider: SearchCriteriaProvider,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : LoadTrendingSearchUseCase {
override suspend fun execute(): List<String> {
return withContext(coroutineDispatcherProvider.io()) {
searchCriteriaProvider.provideTrendingSearch().trendingSearches
}
}
}
Just in case its needed this is my interface:
interface CoroutineDispatcherProvider {
fun io(): CoroutineDispatcher = Dispatchers.IO
fun default(): CoroutineDispatcher = Dispatchers.Default
fun main(): CoroutineDispatcher = Dispatchers.Main
fun immediate(): CoroutineDispatcher = Dispatchers.Main.immediate
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}
class CoroutineDispatcherProviderImp @Inject constructor() : CoroutineDispatcherProvider
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我认为 Jake Wharton 的这个库 https://github.com/cashapp/turbine 会很棒当您将来需要更复杂的场景时,会有所帮助。
我认为正在发生的事情是,在片段中您正在调用
.collect { }
,这确保了流程的启动。检查终端操作符定义:流上的终端操作符正在挂起启动流集合的函数。 https://kotlinlang.org/docs/flow.html#terminal-flow-operators对于sharedFlow 来说情况并非如此,这可能是配置为急切启动。
因此,要解决您的问题,您可能只需致电
I think this library https://github.com/cashapp/turbine by Jake Wharton will be of great help in the future when you need more complex scenarios.
What I think is happening is that in fragment you are calling
.collect { }
and that is ensuring the flow is started. Check the Terminal operator definition: Terminal operators on flows are suspending functions that start a collection of the flow. https://kotlinlang.org/docs/flow.html#terminal-flow-operatorsThis is not true for sharedFlow, which might be configured to be started eagerly.
So to solve your issue, you might just call
这对我有用:
This is what worked for me: