我正在尝试了解 Turbine 如何与
StateFlow
一起工作。
HelloWorldView模型
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))
}
}
}
你好世界状态
data class HelloWorldState(val loading: Boolean = false)
DoOperationUseCase
class DoOperationUseCase {
suspend operator fun invoke(): Result<List<String>> {
delay(500)
return Result.success(listOf("1"))
}
}
HelloWorldViewModel测试
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
}
}
}
结果:
No value produced in 3s
app.cash.turbine.TurbineAssertionError: No value produced in 3s
仅在以下情况下测试成功:
@Test
fun doOperation() = runTest {
coEvery { doOperationUseCase() } returns Result.success(listOf(""))
classUnderTest.state.test {
classUnderTest.doOperation()
assertEquals(classUnderTest.state.value.copy(loading = false), awaitItem())
}
}
依赖关系:
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 文档没有帮助,因为存储库控制何时发出新值。
您可以创建自定义范围来帮助编写测试用例,而不是使用默认的
viewModelScope
扩展属性。
这是上面代码中问题的解决方案。
步骤1
为 ViewModel 创建自定义可关闭范围
import java.io.Closeable
import kotlinx.coroutines.CoroutineScope
import kotlin.coroutines.CoroutineContext
class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
注意:我是直接从aac ViewModel.kt ext(package androidx.lifecycle)
复制的第2步
在 ViewModel 中使用此自定义范围
class HelloWorldViewModel(
private val customScope: CloseableCoroutineScope = CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
private val doOperationUseCase: DoOperationUseCase
): ViewModel(customScope) {
private val _state: MutableStateFlow<HelloWorldState> = MutableStateFlow(HelloWorldState())
val state: StateFlow<HelloWorldState> = _state.asStateFlow()
fun doOperation() {
customScope.launch(dispatcher) {
_state.emit(state.value.copy(loading = true))
doOperationUseCase()
_state.emit(state.value.copy(loading = false))
}
}
}
第3步
然后更新您的测试以使用测试范围和调度程序
@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(MockKExtension::class)
class HelloWorldViewModelTest {
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
var doOperationUseCase = DoOperationUseCase()
private lateinit var classUnderTest: HelloWorldViewModel
@Test
fun doOperation() = runTest(testDispatcher) {
classUnderTest = HelloWorldViewModel(CloseableCoroutineScope(this.coroutineContext), testDispatcher, doOperationUseCase)
assertEquals(HelloWorldState(), classUnderTest.state.value) // expected for initial state
classUnderTest.doOperation()
classUnderTest.state.test {
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
}
}
}
我已经测试了这个解决方案并且它有效。 附加提示: