7

I'm trying to do ViewModel testing using Kotlin(1.6.21) Coroutines(1.6.4) and Kotlin Flow.

Following official Kotlin coroutine testing documentation but ViewModel is not waiting/returning a result for suspending functions before test completion. Have gone through top StackOverflow answers and tried all suggested solutions like injecting the same CoroutineDispatcher, and passing the same CoroutineScope but none worked so far. So here I am posting the current simple test implementation. Have to post all classes code involved in the test case to get a better idea.

ReferEarnDetailViewModel.kt:
Injected Usecase and CoroutineContextProvider and calling API using viewModelScope with provided dispatcher. But after calling callReferEarnDetails() from the test case, it is not collecting any data emitted by the mock use case method. Have tried with the direct repo method call, without Kotlin flow as well but same failure.

@HiltViewModel class 
ReferEarnDetailViewModel @Inject constructor(
  val appDatabase: AppDatabase?,
  private val referEarnDetailsUseCase: ReferEarnDetailsUseCase,
  private val coroutineContextProvider: CoroutineContextProvider) : BaseViewModel() {
  
  fun callReferEarnDetails() {
    setProgress(true)
    viewModelScope.launch(coroutineContextProvider.default + handler) {
        
    referEarnDetailsUseCase.execute(UrlUtils.getUrl(R.string.url_referral_detail))
            .collect { referEarnDetail ->
                parseReferEarnDetail(referEarnDetail)
            }
    }
}

 private fun parseReferEarnDetail(referEarnDetail: 
  ResultState<CommonEntity.CommonResponse<ReferEarnDetailDomain>>) {
   when (referEarnDetail) {
        is ResultState.Success -> {
            setProgress(false)
            .....
       }
    }
  }

ReferEarnCodeUseCase.kt: Returning Flow of Api response.

@ViewModelScoped
class ReferEarnCodeUseCase @Inject constructor(private val repository: 
  IReferEarnRepository) :BaseUseCase {

  suspend fun execute(url: String): 
   Flow<ResultState<CommonEntity.CommonResponse<ReferralCodeDomain>>> {
    return repository.getReferralCode(url)
   }
}

CoroutineTestRule.kt

@ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestDispatcher = 
  StandardTestDispatcher()) : TestWatcher() {

  val testCoroutineDispatcher = object : CoroutineContextProvider {
    override val io: CoroutineDispatcher
        get() = testDispatcher
    override val default: CoroutineDispatcher
        get() = testDispatcher
    override val main: CoroutineDispatcher
        get() = testDispatcher
  }

 override fun starting(description: Description?) {
     super.starting(description)
     Dispatchers.setMain(testDispatcher)
  }

  override fun finished(description: Description?) {
     super.finished(description)
     Dispatchers.resetMain()
  }
}

ReferEarnDetailViewModelTest.kt

@RunWith(JUnit4::class)
@ExperimentalCoroutinesApi
class ReferEarnDetailViewModelTest {
 private lateinit var referEarnDetailViewModel: ReferEarnDetailViewModel
 private lateinit var referEarnDetailsUseCase: ReferEarnDetailsUseCase

 @get:Rule
 val coroutineTestRule = CoroutineTestRule()
 @Mock
 lateinit var referEarnRepository: IReferEarnRepository
 @Mock
 lateinit var appDatabase: AppDatabase
 @Before
 fun setUp() {
    MockitoAnnotations.initMocks(this)
    referEarnDetailsUseCase = ReferEarnDetailsUseCase(referEarnRepository)
    referEarnDetailViewModel = ReferEarnDetailViewModel(appDatabase, 
    referEarnDetailsUseCase , coroutineTestRule.testCoroutineDispatcher)
 }

 @Test
 fun `test api response parsing`() = runTest {
     val data = ResultState.Success( TestResponse() )

     //When
     Mockito.`when`(referEarnDetailsUseCase.execute("")).thenReturn(flowOf(data))
     //Call ViewModel function which further call usecase function.
     referEarnDetailViewModel.callReferEarnDetails()

     //This should be false after API success response but failing here....
     assertEquals(referEarnDetailViewModel.showProgress.get(),false)
   }
 }

Have tried this solution:

  1. How test a ViewModel function that launch a viewModelScope coroutine? Android Kotlin
  2. Inject and determine CoroutineScope on ViewModel creation
Satwinder Singh
  • 627
  • 1
  • 6
  • 23
  • I didn’t read all the details of your question but noticed that you are using `Mockito`. I'm suggesting on you replace it with [Mockk](https://mockk.io/#coroutines) as it supports mocking in coroutines. – Mohamed Wael Sep 12 '22 at 01:29
  • @Mohamed Initially I tried with Mockk but then replaced with default library just for simplicity. – Satwinder Singh Sep 12 '22 at 11:49

1 Answers1

0

As it is stated in the documentation runTest awaits completion of all the launched in its TestScope coroutines (or throws a timeout). But it does so on exit from the test body. In your case assertEquals fails inside the test body, so test fails immediately.

Generally speaking, this mechanism of awaiting completion of all jobs is a mean of preventing leaks and is not suitable for your purpose.

There are two ways to control the coroutines execution inside the test body:

  • Use methods to control virtual time. E.g. advanceUntilIdle should help in this case - use it before asserting the result and it will execute all the tasks scheduled on the given TestDispatcher.
  • Use regular ways to await execution, e.g. return a job and await its' completion before checking the result. This requires some code redesign, but this is a recommended approach. Check out a couple of paragraphs above the Setting the Main dispatcher chapter.
esentsov
  • 6,372
  • 21
  • 28
  • thanks for suggestions. As I mentioned, I am already following official documentation and have tried with "advanceUntilldle" method as well . Since my viewModel() method "callReferEarnDetails()" doesn’t return any value. So don't want to do code changes here. – Satwinder Singh Sep 16 '22 at 14:57