Unit Testing Compose State Change

Stephen Farr
4 min readOct 11, 2022

--

One of the fantastic new features introduced in Jetpack Compose is automatic recomposition on State change. This powerful new functionality helps us reduce the chance of missing re-rendering our UI properly when the state changes but, introduces a new problem. How do we test each of the state changes?

Basic State Change Tests

The most basic way we might want to test how our ViewModel mutates the State is by verifying that the end state matches the expected state. Take the following ViewModel for example.

class SimpleUiViewModel: ViewModel() {
var state by mutableStateOf(
SimpleUiState(
awesomeText = "This is some pretty cool text!",
errorText = "It done be broke",
shouldShowError = false
)
)

fun displayError() {
state = state.copy(shouldShowError = true)
}
}

data class SimpleUiState(
val awesomeText: String,
val errorText: String,
val shouldShowError: Boolean,
)

The displayError method has no intermediary state changes and simply flips a boolean that says the error message/text/spinny flashy image should be shown. This makes it pretty easy for us to write a unit test to cover this functionality using the state the way it is.

class ExampleUnitTest {
@Test
fun `verify error is displayed when it should be`() {
val vm = SimpleUiViewModel()

assertFalse(vm.state.shouldShowError)
vm.displayError()
assertTrue(vm.state.shouldShowError)
}
}

note my test name is terrible I’d suggest not using it. Looking at the above test, we can easily confirm that the initial and end states match our expectations.

Testing a Series of Changes

Not all methods however will be as simple and straightforward as our displayError method. We may encounter instances where we want to do multiple state changes/recompositions in a single method call. Take the following case for example

class SimpleUiViewModel(
private val repository: SimpleRepository = SimpleRepository() //Assume some magic DI here
): ViewModel() {
var state by mutableStateOf(
SimpleUiState(
awesomeText = "This is some pretty cool text!",
errorText = "It done be broke",
)
)

fun fetchSomeStuff() = viewModelScope.launch {
state = state.copy(isLoading = true)
repository.fetchThings()
state = state.copy(isLoading = false)
}

fun displayError() {
state = state.copy(shouldShowError = true)
}
}

class SimpleRepository {
suspend fun fetchThings() = //Assume some suspendable network call is made
}

data class SimpleUiState(
val awesomeText: String,
val errorText: String,
val shouldShowError: Boolean = false,
val isLoading: Boolean = false
)

In our new method fetchSomeStuff we mutate the state twice, once by saying “hey show some loading indicator” followed by hiding it once the network call completes. If all we want to do is make sure that isLoading is false we can easily do it with our code the way it is but, that would be a pretty useless test right? Most likely we would want to verify that the isLoading flag is turned on, then turned off but State will only record the latest value so how do we do that?

Split the function

One way we can achieve this (albeit not my favorite) is to simply split the function up and test each intermediary step.

class SimpleUiViewModel(
private val repository: SimpleRepository = SimpleRepository() //Assume some magic DI here
): ViewModel() {
var state by mutableStateOf(
SimpleUiState(
awesomeText = "This is some pretty cool text!",
errorText = "It done be broke",
)
)

fun showLoading(isLoading: Boolean = true) {
state = state.copy(isLoading = isLoading)
}

fun fetchSomeStuff() = viewModelScope.launch {
repository.fetchThings()
}

fun displayError() {
state = state.copy(shouldShowError = true)
}
}

this way we can have our code call showLoading;fetchSomeStuff;showLoading . While it's not an awesome option in this case it’s definitely worth considering “is my method doing TOO much and should I split it up?”

Use MutableStateFlow with turbine

CashApp created an awesome library called Turbine that allows you to test a series of changes to a Coroutine Flow as they are set. With this in our back pocket we can convert from using mutableStateOf to using a MutableStateFlow to achieve being able to test a series of changes!

class SimpleUiViewModel(
private val repository: SimpleRepository = SimpleRepository() //Assume some magic DI here
): ViewModel() {
var state = MutableStateFlow(SimpleUiState(
awesomeText = "This is some pretty cool text!",
errorText = "It done be broke",
))

fun fetchSomeStuff() = viewModelScope.launch {
state.value = state.value.copy(isLoading = true)
repository.fetchThings()
state.value = state.value.copy(isLoading = false)
}

fun displayError() {
state.value = state.value.copy(shouldShowError = true)
}
}

and with the accompanying test,

@Test
fun `verify we show and hide loading for network calls`() = runTest {
val vm = SimpleUiViewModel()

vm.state.test {
vm.fetchSomeStuff()

assertEquals(false, awaitItem().isLoading)
assertEquals(true, awaitItem().isLoading)
assertEquals(false, awaitItem().isLoading)
}
}

we are easily able to confirm that each step changes as we run the function! Since we are changing from a mutableStateOf to a MutableStateFlow don’t forget to change your composable to use viewModel.state.collectAsState() to preserve the automatic recomposition of our composable.

Conclusion

Now you are empowered to build all the Unit Tests your hearts and souls could desire! Know of a better way to test Compose State change? Feel free to let me know in the comments below!

--

--

Stephen Farr

Engineer with 13+ years building iOS + Android applications.