Android中用JUnit、Mockito和Kotlin Flow测试ViewModel遇NPE及超时问题
问题分析与解决方案
核心错误原因
- 空指针异常根源:
LoginViewModel依赖LoginRepository,但测试中未mockloginRepository.getMpinKey(context)方法,导致返回的Flow为null,调用collect()时触发NPE。 - Mock对象错配:测试中mock了
LoginDataSource,但ViewModel并未直接依赖它,mock的方法不会被触发,导致LiveData从未收到值,抛出TimeoutException。 - Mock实例错误:注释的Success mock代码仅写了类型
Result.Success<MpinKeyData>,未创建具体实例;失败测试中mock的是DataSource方法而非Repository方法。 - 断言逻辑错误:成功测试中
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) } }
额外优化建议
- 移除冗余Dispatcher设置:
runTest会自动使用测试调度器,无需手动设置StandardTestDispatcher和Dispatchers.setMain,简化测试代码。 - 统一命名规范:测试方法名改为
test_getMpinKey_Success这类贴合业务逻辑的命名,避免与实际方法不符的命名。 - 补充Loading状态测试:如果Repository会发射Loading状态,可补充测试用例,mock返回
flowOf(Result.Loading(), successResult),断言LiveData会收到Loading和Success两个值。 - 避免ViewModel传递Context:将Retrofit初始化逻辑移至Repository层,降低ViewModel与Context的耦合度,简化测试。
内容的提问来源于stack exchange,提问作者Cijo K Paul
相关产品推荐
相关产品推荐

