Manuel Lorenzo
11/07/2024, 9:04 PMStylianos Gakis
11/07/2024, 9:05 PMManuel Lorenzo
11/07/2024, 9:48 PMStylianos Gakis
11/07/2024, 9:55 PMManuel Lorenzo
11/08/2024, 8:08 AMimport androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
class CategoriesViewModel(getCategoriesUseCase: GetCategoriesUseCase) : ViewModel() {
val state = getCategoriesUseCase()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
}
Manuel Lorenzo
11/08/2024, 8:08 AMimport app.cash.turbine.test
import dev.mokkery.answering.returns
import dev.mokkery.every
import dev.mokkery.mock
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.asserter
class CategoriesViewModelTest {
private lateinit var sut: CategoriesViewModel
private val categoryDomainModels = getFakeCategoryDomainModelList()
private val getCategoriesUseCase: GetCategoriesUseCase = mock {
every { invoke() } returns flowOf(categoryDomainModels)
}
@Test
fun given_a_use_case_that_returns_a_list_of_categories_when_the_VM_fetches_them_it_should_return_a_list_with_one_CategoryDomainModel() =
runTest {
CategoriesViewModel(getCategoriesUseCase = getCategoriesUseCase).state.stateIn(this)
.test {
skipItems(1) // Skip the initial empty emission
val items = awaitItem() // Wait for the categories to emit
asserter.assertEquals(
"Expected item list size does not match.",
categoryDomainModels.size,
items.size
)
asserter.assertEquals(
"First item does not match.", categoryDomainModels.first(), items.first()
)
cancelAndIgnoreRemainingEvents()
}
}
}
Manuel Lorenzo
11/08/2024, 8:09 AMapp.cash.turbine.TurbineAssertionError: No value produced in 3se
Stylianos Gakis
11/08/2024, 8:48 AM.stateIn(this)
there. .test
should start the collection and make your flow hot already.
Also since you're testing a real ViewModel directly which uses viewModelScope, that internally uses Dispatchers.Main.immediate, which i think is quite tricky to get to work well with coroutine tests.
I think this talk https://zsmb.co/talks/untangling-coroutine-testing/ goes over it. The interesting part is here where it's explained how to replace the main dispatcher with the one made for testsManuel Lorenzo
11/08/2024, 8:54 AMManuel Lorenzo
11/08/2024, 8:58 AMcommonMain
module? 🤔) so I can’t use JUnit’s TestWatcher
Stylianos Gakis
11/08/2024, 9:46 AMbackgroundScope
and then it will use that instead of the main dispatcher when you call .viewModelScopeStylianos Gakis
11/08/2024, 9:48 AMStylianos Gakis
11/08/2024, 9:48 AMManuel Lorenzo
11/08/2024, 9:54 AMviewModelScope: CoroutineScope = Dispatchers.Main + SupervisorJob()
Gives
Type mismatch.
Required:
CoroutineScope
Found:
CoroutineContext
Manuel Lorenzo
11/08/2024, 10:02 AM@Test
fun test() = runTest {
val categories = listOf(
Category(id = 1, name = "Category 1", slug = "category-1"),
Category(id = 2, name = "Category 2", slug = "category-2")
)
everySuspend {
getCategoriesUseCase.invoke()
} returns flowOf(categoryDomainModels)
val viewModel = CategoriesViewModel(getCategoriesUseCase)
viewModel.state.test {
asserter.assertEquals("", categories, awaitItem())
cancelAndIgnoreRemainingEvents()
}
}
Getting:
Expected :[Category(id=1, name=Category 1, slug=category-1), Category(id=2, name=Category 2, slug=category-2)]
Actual :[]
Stylianos Gakis
11/08/2024, 10:04 AMinitialValue = emptyList()
?Manuel Lorenzo
11/08/2024, 10:05 AMStylianos Gakis
11/08/2024, 10:06 AMskipItems(1)
as you did in the original snippet you sent?Manuel Lorenzo
11/08/2024, 10:06 AMawaitItem()
but it didn’t workManuel Lorenzo
11/08/2024, 10:07 AMStylianos Gakis
11/08/2024, 10:07 AMManuel Lorenzo
11/08/2024, 10:08 AMStylianos Gakis
11/08/2024, 10:08 AMStylianos Gakis
11/08/2024, 10:09 AM.value
on it. It's something I've tried some times in the past with good success.Manuel Lorenzo
11/08/2024, 10:10 AMManuel Lorenzo
11/08/2024, 10:10 AM.stateIn
because of some articles I read in Medium and similar about how to properly load data from a VM on startManuel Lorenzo
11/08/2024, 10:10 AMStylianos Gakis
11/08/2024, 10:12 AMstateIn
is in fact the right way to build your UI state emission from your VM.
That has nothing to do with the tests themselves. Even if you were using a MutableStateFlow which you were adjusting yourself, you'd still have a tricky time testing against a StateFlow.
So I don't think stateIn
itself makes testing any harder than it already was