使用 StateFlow 和协程的单元测试 viewModel

发布于 2025-01-12 20:58:53 字数 3233 浏览 0 评论 0原文

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

后来的我们 2025-01-19 20:58:53

我认为 Jake Wharton 的这个库 https://github.com/cashapp/turbine 会很棒当您将来需要更复杂的场景时,会有所帮助。


我认为正在发生的事情是,在片段中您正在调用 .collect { } ,这确保了流程的启动。检查终端操作符定义:流上的终端操作符正在挂起启动流集合的函数。 https://kotlinlang.org/docs/flow.html#terminal-flow-operators

对于sharedFlow 来说情况并非如此,这可能是配置为急切启动。


因此,要解决您的问题,您可能只需致电

val job = launch {
    trendingSearchViewModel.trendingSearchStateFlow.collect()
}

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-operators

This is not true for sharedFlow, which might be configured to be started eagerly.


So to solve your issue, you might just call

val job = launch {
    trendingSearchViewModel.trendingSearchStateFlow.collect()
}
千纸鹤 2025-01-19 20:58:53

这对我有用:

@Test
fun `should get trending search suggestions`() {
    runBlockingTest {
        // Arrange
        val trending1 = UUID.randomUUID().toString()
        val trending2 = UUID.randomUUID().toString()
        val trending3 = UUID.randomUUID().toString()
        val listOfTrending = listOf(trending1, trending2, trending3)

        whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOfTrending)

        /* List to collect the results */
        val listOfEmittedResult = mutableListOf<List<String>>()
        val job = launch {
            trendingSearchViewModel.trendingSearchStateFlow.toList(listOfEmittedResult)
        }

        // Act
        trendingSearchViewModel.getTrendingSearch()

        // Assert
        assertThat(listOfEmittedResult).isNotEmpty()
        verify(loadTrendingSearchUseCase).execute()

        job.cancel()
    }
}

This is what worked for me:

@Test
fun `should get trending search suggestions`() {
    runBlockingTest {
        // Arrange
        val trending1 = UUID.randomUUID().toString()
        val trending2 = UUID.randomUUID().toString()
        val trending3 = UUID.randomUUID().toString()
        val listOfTrending = listOf(trending1, trending2, trending3)

        whenever(loadTrendingSearchUseCase.execute()).thenReturn(listOfTrending)

        /* List to collect the results */
        val listOfEmittedResult = mutableListOf<List<String>>()
        val job = launch {
            trendingSearchViewModel.trendingSearchStateFlow.toList(listOfEmittedResult)
        }

        // Act
        trendingSearchViewModel.getTrendingSearch()

        // Assert
        assertThat(listOfEmittedResult).isNotEmpty()
        verify(loadTrendingSearchUseCase).execute()

        job.cancel()
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文