Testing¶
You may wonder what is the best way to test a FlowReduxStateMachine
?
There are two strategies we want to discuss here in this section:
- functional integration tests: test the whole state machine as a whole.
- Unit tests to test only a certain handler such as
onEnter {}
,on<Action>
and so on.
Functional integration tests with Turbine¶
This is our recommended way for testing FlowReduxStateMachine
.
For this we need Turbine.
Turbine is a library that makes testing Flow
from Kotlin coroutine much easier.
Let’s say we want to test our ItemListStateMachine
.
With turbine we can do that step by ste quite easily:
import kotlinx.coroutines.test.runTest
@Test
fun `state machine starts with Loading state`() = runTest {
val statemachine = ItemListStateMachine(HttpClient())
statemachine.state.test {
// awaitItem() from Turbine waits until next state is emitted.
// FlowReduxStateMachine emits initial state immediately.
assertEquals(Loading, awaitItem())
}
}
@Test
fun `move from Loading to ShowContent state on successful http response`() = runTest {
val items : List<Item> = generatesomeFakeItems()
val httpClient = FakeHttpClient(successresponse = items)
val statemachine = ItemListStateMachine(httpClient)
statemachine.state.test {
assertEquals(Loading, awaitItem()) // initial state
assertEquals(ShowContent(items), awaitItem()) // loading successful --> ShowContent state
}
}
@Test
fun `move from Loading to Error state on error http response`() = runTest {
val exception = IOExpcetion("fake exception")
val httpClient = FakeHttpClient(error = exception)
val statemachine = ItemListStateMachine(httpClient)
statemachine.state.test {
assertEquals(Loading, awaitItem()) // initial state
assertEquals(Error(message = "A network error occurred", countdown = 3), awaitItem())
}
}
We can apply this pattern all the time, but isn’t it a bit annoying to always start our state machine from initial state and have to go thorough all the state transitions until we reach the state we want to test? Well, one nice side effect of using state machines is that you can jump to a certain state right from the beginning. To be able to do that we need to pass the initial state as constructor parameter like this:
class ItemListStateMachine(
private val httpClient: HttpClient,
initialState : ListState = Loading // now constructor parameter
) : <ListState, Action>(initialState) { ... }
Now let’s write a test that checks that pressing the retry button works:
@Test
fun `from Error state to Loading if RetryLoadingAction is dispatched`() = runTest {
val initialState = Error(message = "A network error occurred", countdown = 3)
val statemachine = ItemListStateMachine(httpClient, initialState)
statemachine.state.test {
assertEquals(initialState, awaitItem())
// now we dispatch the retry action
statemachine.dispatch(RetryLoadingAction)
// next state should then be Loading
assertEquals(Loading, awaitItem())
}
}
@Test `once Error countdown is 0 move to Loading state`() = runTest {
val msg = "A network error occurred"
val initialState = Error(message = msg, countdown = 3)
val statemachine = ItemListStateMachine(httpClient, initialState)
statemachine.state.test {
assertEquals(initialState, awaitItem())
assertEquals(Error(msg, 2))
assertEquals(Error(msg, 1))
assertEquals(Error(msg, 0))
assertEquals(Loading, awaitItem())
}
}
Unit testing handlers¶
Another way how you can test your state machines is on unit test level, but it requires that you logic is extracted into functions.
For example, let’s say we want to unit test loadItemsAndMoveToContentOrError()
spec {
inState<Loading> {
onEnter { loadItemsAndMoveToContentOrError(it) }
}
}
suspend fun loadItemsAndMoveToContentOrError(state: State<Loading>): ChangedState<State> {
return try {
val items = httpClient.loadItems()
state.override { ShowContent(items) }
} catch (t: Throwable) {
state.override { Error(message = "A network error occurred", countdown = 3) }
}
}
We can do that as such:
@Test
fun `on http success move to ShowContent state`() = runTest{
val items : List<Item> = generatesomeFakeItems()
val httpClient = FakeHttpClient(successresponse = items)
val statemachine = ItemListStateMachine(httpclient)
val startState = State(Loading) // Create a FlowRedux State object
val changedState : ChangedState<ListState> = statemachine.loadItemsAndMoveToContentOrError(startState)
val result : ListState = changedState.reduce(startState.snapshot) // FlowRedux API: you must call reduce
val expected = ShowContent(items)
assertEquals(expected, result)
}
With FlowRedux you can write unit tests, but there is a bit of overhead:
- You need to wrap the actual state into FlowRedux
State
class. - To get from a
ChangedState
to the actual value you need to call.reduce()
on it.
What we basically have to do here is what FlowRedux does internally. In the future we may provide a more convenient way to write this kind of unit tests with less overhead.