I've implemented Android view model which exposes Compose State<T> instance. For instance, view model implementation:
class MyViewModel<S> {
private val _viewState: MutableState<S> = mutableStateOf(initialState)
val viewState: State<S> = _viewState
...
protected fun setState(newState: S) {
_viewState.value = newState
}
}
I'd like to test in unit tests what values/state it will get set. Just a brief example of what I'd like to achieve:
class MyViewModelTest {
@Test
fun `when view model initialized then should emit initial state first`() {
val viewModel = MyViewModel()
assertEquals(InitialState(), viewModel.viewState.value)
}
@Test
fun `when view model interacted then should emit result state`() {
val viewModel = MyViewModel()
val expectedState = NewState()
viewModel.setState(expectedState)
assertEquals(expectedState, viewModel.viewState.value)
}
}
Is it possible to test State<T>? How do you guys unit test compose states values if you store them in view model side?
For my specific use case, I'm using coroutines and unidirectional data flow, but exposing state via mutableStateOf. Was looking to Unit Test initial state + effect of events as is your case.
class MyViewModel(
// For injecting test dispatcher
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
// State exposed via delegate
var state by mutableStateOf(State.Show())
private set
fun onEvent(event: Event) {
when (event) {
is Event.Greet -> greet(event)
else -> TODO("Other Stuff")
}
}
private fun greet(greet: Event.Greet) {
viewModelScope.launch(dispatcher) {
// Do some, ya know, API stuff!
state = State.Show("Hello ${greet.name}")
}
}
}
state is exposed via delegate, with a private setter. The initial value would be Show("Hello"). We can also set updates directly with the imports:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
The injection of the dispatcher is essential for controlling the progression.
// Encapsulates possible states
sealed class State {
// Data class calculates equality from the constructor
// aiding with assertEquals
data class Show(
val greeting: String = "Hello"
) : State()
}
// ... and possible Events
sealed class Event {
data class Greet(val name: String) : Event()
}
@ExperimentalCoroutinesApi
@RunWith(MockitoJUnitRunner::class)
class MyViewModelTest {
val dispatcher = StandardTestDispatcher()
@Test
fun `viewModel shows initial state`() = runTest {
val vm = MyViewModel(dispatcher)
val expectedState = State.Show()
// Get the initial state
val state = vm.state
assertEquals(expectedState, state)
}
@Test
fun `viewModel shows updated state`() = runTest {
val vm = MyViewModel(dispatcher)
val expectedState = State.Show("Hello Robertus")
// Trigger an event
vm.onEvent(Event.Greet("Robertus"))
// Await the change
dispatcher.scheduler.advanceUntilIdle()
// Get the state
val state = vm.state
assertEquals(expectedState, state)
}
}
The val dispatcher = StandardTestDispatcher() lets us pass in a dispatcher we can control for awaiting the coroutine. We pass this to the ViewModel in the tests and when needing to await them to run and update the state.
Note: the tests are within the expression
= runTest { }.
For the first test, viewModel shows initial state, we get the initial value the mutableStatOf was initialized with.
In the second test, viewModel shows updated state, we await the coroutine to complete (dispatcher to idle) by calling dispatcher.scheduler.advanceUntilIdle() between passing the Event and getting the state.
Without it, we will get the initial state.

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With