A question around testing: I have a pattern I use...
# compose-android
t
A question around testing: I have a pattern I use for ViewModels, where I will often derive a ui state from various flows, during initialisation of the ViewModel:
Copy code
class MyViewModel {
    val uiState = repository.getSomeFlow().map { UiState.Success(it) }
        .stateIn(viewModelScope, WhileSubscribed(), UIState.Loading)
}
When it comes to testing the ViewModel, I’ll have a fake repository where
getSomeFlow()
has a different implementation:
Copy code
class FakeRepository: Repository {

    var onGetSomeFlow: () -> Flow = {
        throw NotImplementedException()
    }

    override fun getSomeFlow(): Flow {
        return onGetSomeFlow()
    }
}
A problem arises though - if I instantiate the ViewModel in the
@Before
test setup function, then the flow value is derived at that point:
Copy code
@Before
fun setup() {
    viewModel = MyViewModel() // the uiState will be derived at this time
}
Then, in an individual test, if I want to change the behaviour of
getSomeFlow()
, it has no effect:
Copy code
@Test
fun MyTest() {
    fakeRepository.onGetSomeFlow = { flowOf(someValue) }

    ...
    testAssertions()
}
One solution, is to instantiate the ViewModel inside each test, after defining the behaviour of the fake repository:
Copy code
@Test
fun MyTest() {
    fakeRepository.onGetSomeFlow = { flowOf(someValue) }
    
    viewModel = MyViewModel(fakeRepository)

    ...
    testAssertions()
}
It’s not ideal - you have to remember to do this for every test (which is essentially what
@Before
is for!)
I’m wondering if there’s a nicer way to solve this problem?
a
ViewModels with side effects upon constructions are so tricky to test. Have your flow start emiting after calling a function. Not after construction
s
Or use a Turbine instead, here’s a place where I use it https://github.com/HedvigInsurance/android/blob/89d0e0ab832f85b34262411a49453f3c12[…]t/src/main/kotlin/com/hedvig/android/auth/FakeAuthRepository.kt In your FakeFoo, make it return a flow normally, and have that flow read from a
Turbine
. Then in your test, emit items to that turbine, like in this test, and it will populate that flow, and in turn this will be emitted to your uiState. It’s super convenient, you don’t get a first emission of something since it’s empty before you give it a value, and you don’t need to deal with `var`s and whatever issues that may bring you.
And reading the comment above mine, this isn’t even a case where it’s a ViewModel doing side effects on initialization, since your uiState only becomes hot
WhileSubscribed
. Usually when that’s a problem when you do stuff in the
init {}
block, but you’re not doing that really. Your ViewModel setup looks perfectly fine, just that the FakeRepository could use some Turbine
d
s
But… it’s already doing that 😅 It’s already emitting only after it’s called due to it being WhileSubscribed, and not during construction. What does happen on construction time is only that the
repository.getSomeFlow()
function is called, which is not an issue actually. It’s only an issue here because the
Fake
implementation has implemented it in this way where it can be lazily changed (since it’s a var) but the VM calls it on start. I feel like there’s a misunderstanding here on what’s happening here vs other VMs which on init {} launch coroutines which is definitely a much different situation
a
Its already emitting o ly after its called due to it being WhileSubscribed
And when is WhileSubscribed called???
Copy code
class MyViewModel(
  private val repo: Repository
) {
  val state = MutableState<Somethint>()

  fun showSomething() {
     repo.getSomeFlow().collect {
        state.value = UiState.Success(it)
     }
  }
}
Now in your test code, you call
showSomething
before your assertions. I hope that is clear
s
This showSomething doesn't compile, since collect is a suspending function but showSomething isn't
Also, if we assume you launch it in viewModelScope, this'd be even worse. If you call showSomething() multiple times, you'll have multiple coroutines collecting and updating the state. Also, if you are putting your app in the background, you will keep collecting the flow and updating the state, even doing
collectStateWithLifecycle
won't help since you'd not be scoping the flow collection to your lifecycle scope. All of this is solved by what was written in the original message, by using .stateIn and WhileSubscribed.
And when is WhileSubscribed called???
That's called on initialization, but that's not a side effect, you're just describing the configuration of your StateFlow, and that it's only going to be active after it's subscribed. Which means in ViewModel init, it's gonna be not active, and only after the first collectStateWithLifecycle is called it will start emitting values. And it will stop collecting after your app goes to the background.
a
Looks like you've got your issue sorted then, right?? I do think in a different approach from yours but sometimes its okay to have a different mindset
s
This isn’t about me, but for what Tim was asking for. Also, about this message, https://kotlinlang.slack.com/archives/C04TPPEQKEJ/p1682060590116479?thread_ts=1681769813.381799&amp;cid=C04TPPEQKEJ what did you actually mean? What approach would you take to make that compile?
a
When it comes to
viewmodels
and repositories. Do nothing during constructions.
What approach would you take to make that compile?
It really depends with your use case. But something in the lines of
Copy code
class MyViewModel(
  private val repo: Repository
) {
  val state = MutableState<Somethint>()

  fun showSomething() = viewModelScope.launch {
     repo.getSomeFlow().collect {
        state.value = UiState.Success(it)
     }
  }
}
If you call showSomething() multiple times, you'll have multiple coroutines collecting and updating the state
Then probably don't call it multiple times?? The same way you are not calling the constructor multiple times. If you want to make sure that its just one coroutine is running, that should reflect in your code then. Something like
Copy code
class MyViewModel(
  private val repo: Repository
) {
  val state = MutableState<Somethint>()

  private var job: Job?
  fun showSomething() {
     if(job != null) return
     job = viewModelScope.launch {
         repo.getSomeFlow().collect {
            state.value = UiState.Success(it)
         }
     }
  }

  override onClose() {
      job.cancel()
      super.onClose()
  }
}
This whole arrangement, makes testing easier, You instantiate all you dependenciess (i.e.
repo
in this example), your viewModel. And then test the behavior of the viewModel when
showSomething()
is called.
d
I agree the above method is more verbose. But it’s also easier to reason about and is reliable.
s
Then probably don’t call it multiple times??
Sure, then you put the burden on the UI and allow it to make a mistake, while what I suggest you can’t make that mistake, you make it impossible. And you may think “just don’t call it twice, I am not stupid”, sure, but if you get a configuration change, and your activity gets recreated, and your UI is starting this flow onCreate, it will in fact call it again. And since the ViewModel will out-live this configuration change, you will in fact trigger this collection twice, so the first suggestion you give is not viable, so we cross that out. So with
Copy code
class MyViewModel(
  private val repo: Repository
) {
  val state = MutableState<Somethint>()

  fun showSomething() = viewModelScope.launch {
     repo.getSomeFlow().collect {
        state.value = UiState.Success(it)
     }
  }
}
• No coroutine starting on construction time. • Calling
showSomething
from your UI starts a coroutine which never stops. If you put the app in the background it still is collecting. If you do a config change and you therefore call it again onCreate you then launch yet another coroutine, collecting this more than once, potentially as many times as you’ve gotten a configuration change With
Copy code
class MyViewModel(
  private val repo: Repository
) {
  val state = MutableState<Somethint>()

  private var job: Job?
  fun showSomething() {
     if(job != null) return
     job = viewModelScope.launch {
         repo.getSomeFlow().collect {
            state.value = UiState.Success(it)
         }
     }
  }

  override onClose() {
      job.cancel()
      super.onClose()
  }
}
• No coroutine starting on construction time. • If you call it more than once you’ve got this safety mechanism which doesn’t let you run multiple collections at the same time, this is good • You now have to keep a Job reference for each collection that you may be doing, scaling terribly if you want to do more like this. Also have to remember to call
cancel
onClose. • If you put the app in the background, nothing stops this collection, and it will keep collecting as long as your ViewModel is alive, despite the app being in the background and are likely not interested in the updates anymore, wasting resources With
Copy code
class MyViewModel(
  private val repo: Repository
) {
  val state: StateFlow<Something> = repo
    .getSomeFlow()
    .map { UiState.Success(it) }
    .stateIn(viewModelScope, WhileSubscribed(), UIState.Loading) // Can add a number inside WhileSubscribed too, so that on a config change you don't start over, but keep it alive for a bit before it dies out
}
you get • No coroutine starting on construction time. • Your UI can’t mess up and call the flow more than once and turn two collections on at the same time • The collection will stop when your UI is not in the resumed state when combined with
collectAsStateWithLifecycle()
• You get super easy testing capabilities by properly faking your
Repository
dependency as I explain here. Same as you’d do with the other solutions too actually.