You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Kotlin:Hilt+Clean Architecture下带Flow的ViewModel测试求助

CoinListViewModel 测试方案建议

依赖准备

确保测试依赖包含以下库(添加到模块build.gradle的testImplementation块):

  • JUnit 5:org.junit.jupiter:junit-jupiter-api:5.9.2、org.junit.jupiter:junit-jupiter-engine:5.9.2
  • Mockito Kotlin:org.mockito.kotlin:mockito-kotlin:4.1.0(简化Mock操作)
  • Kotlin协程测试库:org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3(处理协程与Flow测试)
  • Lifecycle测试库:androidx.lifecycle:lifecycle-viewmodel-ktx-testing:2.6.2(测试ViewModel状态)

核心测试思路

基于Clean架构,我们只需Mock掉GetListOfCoinsUseCaseImpl,验证ViewModel在接收用例返回的不同Resource状态时,是否正确更新对外暴露的state。无需测试用例本身的业务逻辑(属于用例层测试范围),只关注ViewModel的状态转换逻辑。

由于ViewModel使用了viewModelScope和Flow,测试时需用协程测试框架控制调度,确保Flow事件同步执行。

具体测试用例及代码示例

1. 初始化时触发加载状态

验证ViewModel初始化后,用例返回Resource.Loading时,state的isLoading属性为true。

@OptIn(ExperimentalCoroutinesApi::class)
class CoinListViewModelTest {

    private val mockGetCoinsUseCase = mock<GetListOfCoinsUseCaseImpl>()
    private lateinit var viewModel: CoinListViewModel

    @BeforeEach
    fun setup() {
        // 配置Mock用例返回Loading状态的Flow
        whenever(mockGetCoinsUseCase()).thenReturn(flowOf(Resource.Loading))
        // 初始化ViewModel并传入Mock用例
        viewModel = CoinListViewModel(mockGetCoinsUseCase)
    }

    @Test
    fun `initializing should set loading state`() = runTest {
        val currentState = viewModel.state.value
        assert(currentState.isLoading)
        assert(currentState.coins.isEmpty())
        assert(currentState.error == null)
    }
}

2. 成功获取数据时更新列表

验证用例返回Resource.Success带有效数据时,state的coins列表与返回数据一致,且isLoading为false、error为null。

@Test
fun `success response should update coins in state`() = runTest {
    // 准备测试数据
    val testCoins = listOf(Coin(id = "1", name = "Bitcoin"), Coin(id = "2", name = "Ethereum"))
    // 配置Mock用例返回Success状态
    whenever(mockGetCoinsUseCase()).thenReturn(flowOf(Resource.Success(testCoins)))
    
    viewModel = CoinListViewModel(mockGetCoinsUseCase)
    
    val currentState = viewModel.state.value
    assert(!currentState.isLoading)
    assert(currentState.coins == testCoins)
    assert(currentState.error == null)
}

// 测试数据为空的场景
@Test
fun `success response with null data should set empty coins`() = runTest {
    whenever(mockGetCoinsUseCase()).thenReturn(flowOf(Resource.Success(null)))
    
    viewModel = CoinListViewModel(mockGetCoinsUseCase)
    
    val currentState = viewModel.state.value
    assert(currentState.coins.isEmpty())
}

3. 错误状态下更新错误信息

验证用例返回Resource.Error时,state的error属性正确赋值,且isLoading为false、coins为空。

@Test
fun `error response with message should set error in state`() = runTest {
    val testErrorMsg = "Network timeout"
    whenever(mockGetCoinsUseCase()).thenReturn(flowOf(Resource.Error(testErrorMsg)))
    
    viewModel = CoinListViewModel(mockGetCoinsUseCase)
    
    val currentState = viewModel.state.value
    assert(!currentState.isLoading)
    assert(currentState.coins.isEmpty())
    assert(currentState.error == testErrorMsg)
}

// 测试错误消息为null的场景
@Test
fun `error response with null message should set default error`() = runTest {
    whenever(mockGetCoinsUseCase()).thenReturn(flowOf(Resource.Error(null)))
    
    viewModel = CoinListViewModel(mockGetCoinsUseCase)
    
    val currentState = viewModel.state.value
    assert(currentState.error == "Unexpected Error")
}

关键注意事项

  • 协程控制:必须使用runTest包裹测试逻辑,它会自动管理测试协程作用域,确保Flow的onEach块同步执行,避免异步操作导致测试失败。
  • Mock返回值类型:确保Mock用例返回的是Flow类型(因为getAllCoinsUseCase()返回值为Flow),不能直接返回单个Resource对象。
  • 状态验证时机:ViewModel的init块会立即调用getData(),初始化后可直接获取state.value验证(runTest会阻塞直到协程完成)。

内容的提问来源于stack exchange,提问作者sam_1234

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 00:05:28