androidmockitokotlin-flowandroid-mvvmandroid-junit

How to test the ViewModel using Junit, Mockito and Kotlin flows in Android


I tried to test the ViewModel using Junit and Mockito, getting null pointer exception. It returns cannot invoke flow collector. I want to test the responses coming in MpinDataKey, check all parameters are there in MpinDataKey, the api call etc.

Getting following exception :

Exception in thread "Test worker @coroutine#2" java.lang.NullPointerException: Cannot invoke "kotlinx.coroutines.flow.Flow.collect(kotlinx.coroutines.flow.FlowCollector, kotlin.coroutines.Continuation)" LiveData value was never set. java.util.concurrent.TimeoutException: LiveData value was never set.atLiveDataUtilTestKt.getOrAwaitValueTest(LiveDataUtilTest.kt:38)

data class MpinKeyData( val token: String, val publicKey: String)
sealed class Result<T> { 
class Loading<T>: Result<T>()
data class Success<T>(val data: T): Result<T>()
data class Error<T>(val errorMessage: String): Result<T>() 
} 
class LoginViewModel @Inject constructor(private val loginRepository: LoginRepository): ViewModel() {

    private var _mpinKeyResponse = MutableLiveData<Result<MpinKeyData>>()
    val mpinKeyResponse: LiveData<Result<MpinKeyData>>
        get() = _mpinKeyResponse

    
    fun getMpinkey(context: Context) {
        viewModelScope.launch {
            loginRepository.getMpinKey(context).collect() {
                _mpinKeyResponse.postValue(it)
            }
        }
    }
}
class LoginDataSource @Inject constructor() {

suspend fun getMpinKey(context: Context): Result<MpinKeyData> { 
return try { 
val retrofit = RetrofitBuilder.getUnAuthRetrofit(context)
val loginService: LoginService by lazy { retrofit.create(LoginService::class.java) 
  } 
val response = loginService.getMpinKey() ResponseHelper.processResponse(response)
}catch (e: Exception) {
Result.Error(e.message ?: "Exception Found") 
   } 
 } 
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValueTest(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(value: T) {
            data = value
            latch.countDown()
            this@getOrAwaitValueTest.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}
class LoginViewModelTest {

    @Mock
    private lateinit var loginRepository: LoginRepository

    @Mock
    private lateinit var loginDataSource: LoginDataSource

    private val testDispatcher = StandardTestDispatcher()

    @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)
        Dispatchers.setMain(testDispatcher)
    }

    @ExperimentalCoroutinesApi
    @Test
    fun test_beginEnroll_Success() = runTest{
//        Mockito.`when`(loginDataSource.getMpinKey(context)).thenReturn(Result.Success<MpinKeyData>)
        val vmodel = LoginViewModel(loginRepository)
        vmodel.getMpinkey(context)
        testDispatcher.scheduler.advanceUntilIdle()
        val  result = vmodel.mpinKeyResponse.getOrAwaitValueTest()
        Assert.assertEquals(0,result)
    }

    @ExperimentalCoroutinesApi
    @Test
    fun test_beginEnroll_Failure() = runTest{
        Mockito.`when`(loginDataSource.getMpinKey(context)).thenReturn(Result.Error("Something went wrong"))
        val vmodel = LoginViewModel(loginRepository)
        vmodel.getMpinkey(context)
        testDispatcher.scheduler.advanceUntilIdle()
        val  result = vmodel.mpinKeyResponse.getOrAwaitValueTest()
        Assert.assertEquals(1,result is Result.Error)
    }


    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }

}

Solution

  • @ExperimentalCoroutinesApi
      @Test
      fun test_beginEnroll_Success() = runTest{
              doReturn(flowOf(Result.Success(data = emptyList<MpinKeyData>()))).`when`(loginRepository).getMpinKey(context)
              loginRepository.getMpinKey(context).test{
                  assertEquals(Result.Success(emptyList<List<MpinKeyData>>()), awaitItem())
                  cancelAndIgnoreRemainingEvents()
              }
              verify(loginRepository).getMpinKey(context)
      }
    
      @ExperimentalCoroutinesApi
      @Test
      fun test_beginEnroll_Failure() = runTest{
    
          val errorMessage = "Something went wrong"
    
          doReturn(flowOf(Result.Error<MpinKeyData>(errorMessage))).`when`(loginRepository).getMpinKey(context)
    
          loginRepository.getMpinKey(context).test {
              assertEquals(
                  Result.Error<MpinKeyData>(errorMessage),
                  awaitItem()
              )
              cancelAndIgnoreRemainingEvents()
          }
          verify(loginRepository).getMpinKey(context)
      }