为什么 ViewModel 的 StateFlow 在单元测试中不更新,而是使用 stateIn 从存储库映射到 UI 状态?

问题描述 投票:0回答:1

我正在使用假存储库测试 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 }
    }
}

MedicinesViewModel

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
        )
}

MedicinesViewModelTest

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,但没有成功。

android kotlin viewmodel android-unit-testing kotlinx.coroutines.flow
1个回答
0
投票

看起来我忘记在单元测试中设置主调度程序,这就是为什么

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>()
    }

}

直接来自于测试协程的官方文档。

© www.soinside.com 2019 - 2024. All rights reserved.