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.