Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Test Android Compose State values

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?

like image 261
Robertas Setkus Avatar asked Apr 10 '26 00:04

Robertas Setkus


1 Answers

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.

View Model:

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.

States & Events, for completeness

// 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()
}

With this setup, we can get to the unit testing:

@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. enter image description here

like image 64
Shazbot Avatar answered Apr 11 '26 13:04

Shazbot



Donate For Us

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