I'm trying to understand how Turbine works with StateFlow
.
HelloWorldViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class HelloWorldViewModel(
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val doOperationUseCase: DoOperationUseCase
) : ViewModel() {
private val _state: MutableStateFlow<HelloWorldState> = MutableStateFlow(HelloWorldState())
val state: StateFlow<HelloWorldState> = _state.asStateFlow()
fun doOperation() {
viewModelScope.launch(dispatcher) {
_state.emit(state.value.copy(loading = true))
doOperationUseCase()
_state.emit(state.value.copy(loading = false))
}
}
}
HelloWorldState
data class HelloWorldState(val loading: Boolean = false)
DoOperationUseCase
class DoOperationUseCase {
suspend operator fun invoke(): Result<List<String>> {
delay(500)
return Result.success(listOf("1"))
}
}
HelloWorldViewModelTest
import app.cash.turbine.test
import io.mockk.coEvery
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(MockKExtension::class)
class HelloWorldViewModelTest {
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
@MockK
lateinit var doOperationUseCase: DoOperationUseCase
private lateinit var classUnderTest: HelloWorldViewModel
@BeforeEach
fun setUp() {
Dispatchers.setMain(testDispatcher)
classUnderTest = HelloWorldViewModel(testDispatcher, doOperationUseCase)
}
@AfterEach
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun doOperation() = runTest {
coEvery { doOperationUseCase() } returns Result.success(listOf(""))
assertEquals(HelloWorldState(), classUnderTest.state.value) // expected for initial state
classUnderTest.state.test {
classUnderTest.doOperation()
assertEquals(classUnderTest.state.value.copy(loading = true), awaitItem()) // expected before calling doOperationUseCase use case
assertEquals(classUnderTest.state.value.copy(loading = false), awaitItem()) // expected after calling doOperationUseCase use case
}
}
}
Result:
No value produced in 3s
app.cash.turbine.TurbineAssertionError: No value produced in 3s
Only test success when:
@Test
fun doOperation() = runTest {
coEvery { doOperationUseCase() } returns Result.success(listOf(""))
classUnderTest.state.test {
classUnderTest.doOperation()
assertEquals(classUnderTest.state.value.copy(loading = false), awaitItem())
}
}
Dependencies:
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
testImplementation("org.junit.platform:junit-platform-console:1.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
testImplementation("org.junit.jupiter:junit-jupiter-params:5.8.2")
testImplementation("app.cash.turbine:turbine:1.0.0")
testImplementation("io.mockk:mockk:1.13.8")
testImplementation("com.google.truth:truth:1.1.4")
Google documentation is not helpful as the repository controls when to emit a new value.