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

Android中用JUnit、Mockito和Kotlin Flow测试ViewModel遇NPE及超时问题

问题分析与解决方案

核心错误原因

  1. 空指针异常根源:LoginViewModel依赖LoginRepository,但测试中未mockloginRepository.getMpinKey(context)方法,导致返回的Flow为null,调用collect()时触发NPE。
  2. Mock对象错配:测试中mock了LoginDataSource,但ViewModel并未直接依赖它,mock的方法不会被触发,导致LiveData从未收到值,抛出TimeoutException。
  3. Mock实例错误:注释的Success mock代码仅写了类型Result.Success<MpinKeyData>,未创建具体实例;失败测试中mock的是DataSource方法而非Repository方法。
  4. 断言逻辑错误:成功测试中Assert.assertEquals(0,result)完全不符合预期;失败测试中Assert.assertEquals(1,result is Result.Error)将Boolean与整数比较,逻辑错误。

修正后的测试代码

首先假设LoginRepository的getMpinKey方法签名如下(若实际签名不同,只需调整mock逻辑):

interface LoginRepository {
    fun getMpinKey(context: Context): Flow<Result<MpinKeyData>>
}

修正后的测试类:

class LoginViewModelTest {

    @Mock
    private lateinit var loginRepository: LoginRepository

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @ExperimentalCoroutinesApi
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private val context: Context = mock(Context::class.java)

    @Before
    fun initSetUp(){
        MockitoAnnotations.openMocks(this)
    }

    @ExperimentalCoroutinesApi
    @Test
    fun test_getMpinKey_Success() = runTest{
        // 准备测试数据
        val testMpinKeyData = MpinKeyData(token = "test-token", publicKey = "test-public-key")
        val successResult = Result.Success(testMpinKeyData)
        
        // Mock Repository方法,返回包含成功结果的Flow
        Mockito.`when`(loginRepository.getMpinKey(context)).thenReturn(flowOf(successResult))
        
        // 初始化ViewModel并调用目标方法
        val vmodel = LoginViewModel(loginRepository)
        vmodel.getMpinkey(context)
        
        // 获取LiveData值并断言
        val result = vmodel.mpinKeyResponse.getOrAwaitValueTest()
        assert(result is Result.Success)
        val successData = (result as Result.Success<MpinKeyData>).data
        Assert.assertEquals("test-token", successData.token)
        Assert.assertEquals("test-public-key", successData.publicKey)
    }

    @ExperimentalCoroutinesApi
    @Test
    fun test_getMpinKey_Failure() = runTest{
        // 准备错误结果
        val errorMessage = "Something went wrong"
        val errorResult = Result.Error<MpinKeyData>(errorMessage)
        
        // Mock Repository方法,返回包含错误结果的Flow
        Mockito.`when`(loginRepository.getMpinKey(context)).thenReturn(flowOf(errorResult))
        
        // 初始化ViewModel并调用目标方法
        val vmodel = LoginViewModel(loginRepository)
        vmodel.getMpinkey(context)
        
        // 获取LiveData值并断言
        val result = vmodel.mpinKeyResponse.getOrAwaitValueTest()
        assert(result is Result.Error)
        val errorData = (result as Result.Error<MpinKeyData>)
        Assert.assertEquals(errorMessage, errorData.errorMessage)
    }
}

额外优化建议

  1. 移除冗余Dispatcher设置:runTest会自动使用测试调度器,无需手动设置StandardTestDispatcher和Dispatchers.setMain,简化测试代码。
  2. 统一命名规范:测试方法名改为test_getMpinKey_Success这类贴合业务逻辑的命名,避免与实际方法不符的命名。
  3. 补充Loading状态测试:如果Repository会发射Loading状态,可补充测试用例,mock返回flowOf(Result.Loading(), successResult),断言LiveData会收到Loading和Success两个值。
  4. 避免ViewModel传递Context:将Retrofit初始化逻辑移至Repository层,降低ViewModel与Context的耦合度,简化测试。

内容的提问来源于stack exchange,提问作者Cijo K Paul

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 00:42:32