Testing¶
You may wonder what is the best way to test a FlowReduxStateMachineFactory.
There are two strategies we want to discuss 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 a Flow from Kotlin coroutines much easier.
Let’s say we want to test our ItemListStateMachineFactory.
With Turbine we can do that step by step quite easily:
import kotlinx.coroutines.test.runTest
@Test
fun `state machine starts with Loading state`() = runTest {
val stateMachine = ItemListStateMachineFactory(HttpClient()).shareIn(backgroundScope)
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 = ItemListStateMachineFactory(httpClient).shareIn(backgroundScope)
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 = IOException("fake exception")
val httpClient = FakeHttpClient(error = exception)
val stateMachine = ItemListStateMachineFactory(httpClient).shareIn(backgroundScope)
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 the initial state and have to go through 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, by calling initializeWith { ... } before starting the state machine.
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 factory = ItemListStateMachineFactory(httpClient, initialState)
factory.initializeWith { initialState }
val stateMachine = ItemListStateMachineFactory(httpClient).shareIn(backgroundScope)
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
fun `once Error countdown is 0 move to Loading state`() = runTest {
val msg = "A network error occurred"
val initialState = Error(message = msg, countdown = 3)
val factory = ItemListStateMachineFactory(httpClient, initialState)
factory.initializeWith { initialState }
val stateMachine = ItemListStateMachineFactory(httpClient).shareIn(backgroundScope)
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 you can test your state machines is at the unit test level, but it requires that your 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 = ItemListStateMachineFactory(httpClient)
val startState = ChangeableState(Loading) // Create a FlowRedux ChangeableState 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
ChangeableStateclass. - To get from a
ChangedStateto 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.