我正在使用假存储库测试 ViewModel,该存储库使用
StateFlow
来存储假数据。此 StateFlow 作为存储库中的普通 Flow
公开。在 ViewModel 中,我将从存储库接收到的数据映射到 UiState 类。当我使用本地测试来测试 UiState 时,在添加新条目后我没有获得更新的 UiState,即我停留在 Loading 状态并且没有获得任何新更新。这是视图模型、存储库和测试类的文件。
class FakeMedicineRepository : MedicineRepository {
private val _medicines = MutableStateFlow<List<Medicine>>(emptyList())
override val allMedicines: Flow<List<Medicine>> = _medicines.asStateFlow()
suspend fun emit(value: List<Medicine>) = _medicines.emit(value)
override suspend fun addMedicine(
name: String,
purchasePrice: BigDecimal,
sellingPrice: BigDecimal
) {
_medicines.update {
it.plus(
Medicine(
it.size + 1L,
name,
purchasePrice,
sellingPrice
)
)
}
}
override suspend fun isNameTaken(name: String): Boolean {
return _medicines.value.any { it.name == name }
}
}
class MedicinesViewModel(
medicineRepository: MedicineRepository
) : ViewModel() {
val uiState = medicineRepository.allMedicines
.map {
MedicinesUiState.Success(it.map { it.toUiState() })
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MedicinesUiState.Loading
)
}
class MedicinesViewModelTest {
private lateinit var repository: FakeMedicineRepository
private lateinit var viewModel: MedicinesViewModel
@Before
fun setup() {
repository = FakeMedicineRepository()
viewModel = MedicinesViewModel(repository)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `when observe medicines should return empty list`() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.uiState.collect {
println("State: $it")
}
}
var uiState = viewModel.uiState.value
uiState.shouldBeTypeOf<MedicinesUiState.Loading>()
repository.addMedicine("Test", 0.toBigDecimal(), 0.toBigDecimal())
// This assertion fails
viewModel.uiState.value.shouldBeTypeOf<MedicinesUiState.Success>()
}
}
我遵循了在 Android 上测试 Kotlin 流程 中的测试指南。我正在使用那里提到的确切步骤,唯一的区别是它们在存储库中使用
SharedFlow
。但我也尝试用 SharedFlow 替换假存储库中的 StateFlow,但没有成功。
看起来我忘记在单元测试中设置主调度程序,这就是为什么
StateFlow
集合没有发生在 ViewModel 内部。我在测试协程的文档中看到过它,并且每当我们测试在viewModelScope
中启动的协程时就使用它,因为它使用硬编码的主调度程序。
但是我在测试流程的文档中忽略了它,然后不要在任何地方提及它,只需编写测试用例而不设置调度程序。
无论如何,这是最终的工作测试文件:
class MedicinesViewModelTest {
private lateinit var repository: FakeMedicineRepository
private lateinit var viewModel: MedicinesViewModel
@get:Rule
val mainRule = MainDispatcherRule()
@Before
fun setup() {
repository = FakeMedicineRepository()
viewModel = MedicinesViewModel(repository)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `when observe medicines should return empty list`() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.uiState.collect {
println("State: $it")
}
}
var uiState = viewModel.uiState.value
uiState.shouldBeTypeOf<MedicinesUiState.Loading>()
repository.addMedicine("Test", 0.toBigDecimal(), 0.toBigDecimal())
// This assertion is now working
viewModel.uiState.value.shouldBeTypeOf<MedicinesUiState.Success>()
}
}
并且,这是我在上面的测试文件中添加的调度程序规则:
class MedicinesViewModelTest {
private lateinit var repository: FakeMedicineRepository
private lateinit var viewModel: MedicinesViewModel
@get:Rule
val mainRule = MainDispatcherRule()
@Before
fun setup() {
repository = FakeMedicineRepository()
viewModel = MedicinesViewModel(repository)
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `when observe medicines should return empty list`() = runTest {
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.uiState.collect {
println("State: $it")
}
}
var uiState = viewModel.uiState.value
uiState.shouldBeTypeOf<MedicinesUiState.Loading>()
repository.addMedicine("Test", 0.toBigDecimal(), 0.toBigDecimal())
// This assertion is now working
viewModel.uiState.value.shouldBeTypeOf<MedicinesUiState.Success>()
}
}
直接来自于测试协程的官方文档。