An App With SCA: Flow Testing
By: Riccardo Cipolleschi
Tags: Swift Programming, App Development, iOS, Testing, Apple

Last week we discussed how to export the dependencies of our Snake app to gain full control over our codebase. We also finalized the test of the state we were not able to finish a couple of weeks ago.

In the process of factoring out the dependencies, we worked on the timer, moving it from the Reducer to the Environment to have the possibility to control it. However, we did not use it in our tests.

Today, we complete the test of our app, showing an interesting property of the Composable Architecture Test Support: thanks to its ergonomic API, it is possible to fully test our app in a very simple way.

Our codebase so far

In order to properly understand our tests, let me report here the pieces of the app we need.

The first element we need to recall is the Environment . This is the part of the app that gathers all the dependencies used to interact with the external world. The Environment is a container for a set of API: we use a version of those API for the production environment, but we use some mocked implementations in the test environment, to properly control them.

Another piece of code we need to remember is the Reducer . The reducer is a pure function that takes the current State , the Action performend, and the Environment and it updates the State . This is the single point where we implement the whole logic of our app. This is the function that we are going to test.

Lastly, let’s recall the State and the Action . They are pretty simple: the State contains the structure that describes the Snake , the current location of the Mouse , and whether we should present an alert or not.

The Action is an enum with the cases handled by the Reducer .

For the sake of completeness, this is what they look like:

Preparing the test

Now, let’s set up the testing environment.

As already stated, we would like to fully control our test environment. That means, for example, that we need to know exactly what is returned by the dependencies and when the publishers publish a new value. And the value they publish, of course.

Luckily, this is pretty simple to achieve. We defined our Environment so that we can plug the implementation we want. Thus, we can write a mock version of the environment as it follows:

In this mocked environment, we are going to use an RNG that always returns 0 . This can be used to compute a new location for the Mouse . We also pass a parameter that is an Effect , generic in the types produced by the timer. This step is important because it allows us to pass a Publisher that we fully control as a dependency for the reducer. Our test can be deterministic and fully predictable in this way.

From the theory, we know that a good test should follow the AAA rule:

A rrange

rrange A ct

ct A ssert

We are not done with the Arrange part. We created the dependency we need, but we still need to assemble it into the test. So, let’s move to the actual test to complete the first step:

I omitted the Act and Assert to focus on the Arrange part. In the testGameFlow() function, we are creating a PassthroughSubject . This is an object of the Combine framework that can act both as a Publisher and as a Subscriber . We are going to use it mainly as a publisher in our test.

Then we create the Snake for the initial state. Finally, we create the TestStore using another zeroGenerator that will place the initial mouse in the top-left corner of the game field. For the Environment , we use the mock we prepared, passing the PassthroughtSubject as an effect.

At this point, our test has all the ingredients to be finally written, so… let’s cook it!

Writing the Test

If you remember from the State Testing article, the ComposableArchitectureTestSupport allows us to Act and Assert in a single step, thanks to several utility functions and types the good guys of pointfree.co prepared for us.

In short, the TestStore has an assert method that lets us specify a set of Step s to simulate the evolution of our app. There are different types of steps: one to simulate an action entering in the reducer, another to simulate an action received from an effect, a generic step to perform some operation, and a step to update the environment.

By chaining wisely these steps, we can simulate a whole execution flow for the app. We can for, example, startGame , move the snake, changeCurrent(direction:) , move the snake again, and so on.

After every step, we have the possibility to describe how the resulting state should be. For example, after a move action, we describe the new positions for the snake’s head and for its body. After a changeCurrent(direction:) , we describe where the snake is facing, and so on. If any of these descriptions does not match the actual execution outcome, the test fails.

Now, let’s see what the final aspect of this test is:

The first 8 lines of this snippet are the same lines of the Arrange step of the previous one.

The interesting part comes after line 10. In the TestStore.assert function we pass a set of steps that describes the evolution of a game. After the game start, for example, we update the current direction to up .

At this point, we want to make the snake move by one step. In the actual snake game, this action is triggered by the timer. In the tests, we have to simulate that and we can do it by leveraging our PassthroughSubject publisher. The do step lets us access the subject and use it to send a new DispatchTime . This simulates the firing of the timer. If we look at the reducer, we know that every time the timer fires, a move action is injected in the reducer. Therefore, we can handle it by using the receive step and describing the new expected state after the snake has moved.

We then perform the same set of changeCurrent(direction:) , send a DispatchTime through a do step, and receiving a move action a couple more time to make the snake move left and down .

This last movement, however, has a slightly different outcome: before the last move , the snake was facing its own body. By moving toward it, the game ends. This condition is captured at line 48, by describing the expected state as having an AlertState with the "Game Over" title, a message stating the exact length of the snake and an Ok button.

Remember, the Store is still subscribed to the timer Effect . We need to clean up our resources otherwise the ComposableArchitecture would warn us with the following message:

The last do step at line 54 does exactly that: by signaling the completion of the publisher, the Store is able to cancel the subscription to it and the cleanup is performed.

Conclusion

In this article, we harnessed the power of a completely controlled Environment . We have been able to fully test the execution of the app: from the startGame action to the final move that lead to the Game Over alert to be presented.