1

I have a ViewModel that uses StateFlow.asLiveData() to expose a Repository class's StateFlow items and I'm trying to write a test for the ViewModel. My tests are configured with a Mock of an Observer<LoadingStatus> on the ViewModel's exposed LiveData.

The code I'm testing calls this method to update its loading status:

suspend fun MutableStateFlow<LoadingStatus>.performWithStatusUpdates(operation: suspend () -> Unit) {
  this.value = LoadingStatus.Loading()
  try {
    operation.invoke()
    this.value = LoadingStatus.Success()
  } catch (e: Throwable) {
    this.value = LoadingStatus.Error(e)
  }
}

My tests look something like this:

fun testSomething() = runTest {
  viewModel.doSomething()
  advanceUntilIdle()
  argumentCaptor<LoadingStatus>().apply {
    verify(loadingStatusObserver, atLeast(2)).onChanged(capture())
    assertTrue(allValues.any { it is LoadingStatus.Loading })
    assertTrue(allValues.any { it is LoadingStatus.Success })
  }
}

The ViewModel contains code like this:

val loadingStatus = repository.loadingStatusObservable.asLiveData(
  viewModelScope.coroutineContext + ioDispatcher
) // when running tests, ioDispatcher is a StandardTestDispatcher passed into the viewModel 

fun doSomething() {
  viewModelScope.launch(ioDispatcher) {
    repository.doSomething()
  }
}

And the repository does something like this:

val loadingStatusObservable = MutableStateFlow<LoadingStatus>(LoadingStatus.Idle())

suspend fun doSomething() {
  loadingStatusObservable.performWithStatusUpdates {
    apiService.doSomethingElse()
  }
}

The repository has similar tests that call doSomething() and verify that the status goes to loading and then success, and they pass, but the view model ones fail to pick up the Loading status. If I comment out the line in performWithStatusUpdates that sets the status to success after the operation, the tests do pick up the Loading status, so I'm convinced it's something to do with timing. I have run this code with print statements and debuggers and verified that the status is updating correctly, but the change isn't getting picked up by the observers.

How can I make an observer on a StateFlow.asLiveData() detect all changes, even when they're quickly followed by another, different change?

cjs_thm
  • 11
  • 1
  • LiveData simply doesn't work that way. It will only emit the latest value that has arrived since the previous loop of the main thread, since it only emits values on the main thread. If your suspending operation happens faster than the time between two main thread loops, the user will never see that it's loading, because it "loads" faster than a frame of UI animation. I suppose you could insert a `withContext(Dispatchers.Main) {}` block inside your lambda that you pass to `performWithStatusUpdates` to force it to defer completion for at least one loop of the main thread. – Tenfour04 Jan 11 '23 at 18:27
  • I'm using a test dispatcher, so everything should be executing on the same thread. This isn't an instrumented test, there's no UI. – cjs_thm Jan 11 '23 at 18:36
  • I haven't done much unit testing with LiveData, but I assume LiveData doesn't care what dispatcher you are supplying values to it with. LiveData internally doesn't even acknowledge the existence of Kotlin or coroutines. – Tenfour04 Jan 11 '23 at 18:38

0 Answers0