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

测试ViewModel中MutableState更新时断言失败,该如何解决?

问题描述

我编写了如下的PhotoViewModel:

class PhotoViewModel(
    private val getPhotoUseCase: GetPhotoUseCase
) : ViewModel() {

    val photo: MutableState<PhotoItem?> =
        mutableStateOf(null)

    fun getPhotoDetail(photoId: Int) {
        viewModelScope.launch {
            val photo = getPhotoUseCase(photoId)
            photoDetail.value = photo
        }
    }
}

尝试通过以下代码测试状态是否正确更新:

@Test
fun `state is updated correctly`() = runTest {
    coEvery {
        getPhotoDetail(id = 0)
    } returns PhotoItem(
        ...
    )

    systemUnderTest.getPhotoDetail(id = 0)
    val actualState = systemUnderTest.photoDetail.value

    assertEquals(mockedPhotoItemUiModel, actualState)
}

测试失败,报错信息显示断言时比较的是对象内存引用:

Expected :com.package.PhotoItem@3791f50e
Actual   :com.package.PhotoItem@574b7f4a

请问是否应该使用其他断言方法?当前测试MutableState的方式是否完全错误?找不到测试MutableState的官方资料,仅找到一篇英文Stack Overflow帖子。


解决方案

1. 核心问题:对象相等性判断失败

测试失败的根本原因不是MutableState的测试方式错误,而是PhotoItem没有正确实现值相等性。默认情况下,Kotlin类的equals方法比较对象引用,因此即使两个PhotoItem属性完全一致,只要是不同实例,assertEquals就会失败。

2. 修复对象相等性

给PhotoItem添加值相等的实现,有两种常用方式:

  • 将PhotoItem定义为data class:Kotlin的data class会自动生成基于属性的equals、hashCode和toString方法,直接解决值相等问题:
    data class PhotoItem(
        val id: Int,
        val url: String,
        // 其他属性
    )
    
  • 若无法改为data class,手动重写equals和hashCode方法:
    class PhotoItem(
        val id: Int,
        val url: String
    ) {
        override fun equals(other: Any?): Boolean {
            if (this === other) return true
            if (javaClass != other?.javaClass) return false
    
            other as PhotoItem
    
            if (id != other.id) return false
            if (url != other.url) return false
    
            return true
        }
    
        override fun hashCode(): Int {
            var result = id
            result = 31 * result + url.hashCode()
            return result
        }
    }
    

3. MutableState的测试方式无需推翻

你的测试思路是正确的:

  • 使用runTest处理viewModelScope的协程逻辑是标准做法
  • 调用方法后直接获取photoDetail.value的方式可行,runTest会等待协程执行完成

若担心异步更新的不确定性,可添加advanceUntilIdle()确保所有协程执行完毕:

@Test
fun `state is updated correctly`() = runTest {
    val mockedPhoto = PhotoItem(id = 0, url = "test-url")
    coEvery { getPhotoUseCase(id = 0) } returns mockedPhoto

    systemUnderTest.getPhotoDetail(0)
    advanceUntilIdle() // 等待协程完成状态更新

    assertEquals(mockedPhoto, systemUnderTest.photoDetail.value)
}

4. 可选的其他断言方法

若不想修改PhotoItem的equals实现,可使用断言库的属性匹配功能,比如AssertJ:

import org.assertj.core.api.Assertions.assertThat

@Test
fun `state is updated correctly`() = runTest {
    val mockedPhoto = PhotoItem(id = 0, url = "test-url")
    coEvery { getPhotoUseCase(id = 0) } returns mockedPhoto

    systemUnderTest.getPhotoDetail(0)
    advanceUntilIdle()

    val actual = systemUnderTest.photoDetail.value
    assertThat(actual)
        .isNotNull
        .hasFieldOrPropertyWithValue("id", 0)
        .hasFieldOrPropertyWithValue("url", "test-url")
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 06:30:49