View Event Testing in Swift
The Humble Object Pattern in Swift article demonstrated a way of testing the UI update logic. However, it didn’t provide an example of handling and testing view events. Which object should be responsible for the event handling? Should it be a presenter, like VIPER proposes?
Introduction to VIPER states that a presenter contains view logic for preparing content for display and reacting to user inputs. This definition reveals two problems.
First, if the presenter prepares content and reacts to inputs, this sounds very much like a Single Responsibility Principle violation.
Second, this approach imposes a circular dependency between the presenter and the interactor. This is often considered to be a design smell, which leads to further implications. For example, inability to compose the graph of objects using the constructor dependency injection.
The idea of handling events in a presenter is not new to VIPER and surely has its use. But can it be improved?
Flow of Control
The Clean Architecture, the architecture which VIPER applies to iOS development, illustrates the typical control flow. An event starts in a controller, goes through an interactor, and ends up in a presenter.
The controller here is not necessarily a view controller. It’s just some object, other than the presenter, that is the first one along the way of handling events. To avoid confusion, it’ll be referred to as an event handler in this article.
On the diagram, it can be seen that the event handler depends on the interactor to implement the use case. The interactor depends—via the output port it defines using a protocol—on the presenter to deliver the result.
Each object focuses on a single concern. The dependencies of each object can be passed in the constructor providing the maximum safety and help from the compiler.
Breaking Circular Dependencies
Although the concerns are now separated, circular dependencies still exist when the whole object graph, including the view, is taken into account.
The whole graph looks like this: view–event handler–interactor–presenter–view, and it can’t be composed in the initializers. To create the view, an event handler has to be created, which requires an interactor, which requires a presenter, which requires a view. How can this be resolved?
Factories and observer pattern can become handy for breaking circular dependencies.
Instead of taking an already instantiated interactor object, the event handler can depend on a factory that knows how to create interactors. The first thought could also be to make the event handler create interactors directly. However, that would undermine all dependency injection advantages, testability being one of them.
The example shows how a GPS tracking app could present track summary and handle a track name change actions. Because this article is about the event handling, presenter tests will not be shown here and can be found in the previous article.
The interactors are basically commands. The code that executes them may not care about how they are created. Therefore, the interactors have only one method.
There are two interactors conforming to this protocol.
The TrackSummaryInteractor is responsible for preparing the track summary data for display.
The TrackNameUpdateInteractor is responsible for updating the track name in the store.
Both interactors need a persistent store, which is represented by a protocol. The concrete implementation of the persistent store is not interactors’ business and out of the scope of this article.
The track summary data structure contains an identifier that might not be displayed to the user but is used for updating the track name.
It is trivial to test the interactors by writing test doubles that implement the boundary protocols.
The TrackSummaryPresenter is responsible for preparing data for presentation.
The view is defined as a protocol inheriting the presenter’s output protocol. It is not uncommon for the more complex views to inherit multiple protocols declared by several presenters.
The protocol defining the view events is to be implemented by the view observers. The view parameter makes it possible to break the circular dependency.
Due to its tight coupling with the view, the view controller, as discussed before, represents the view to the outside world. It’s the view controller who implements the TrackSummaryView protocol.
The view controller is very humble, passively accepting the data for presentation and delegating event handling to the observer. Because of the constructor injection, the view controller can’t be created without an observer by mistake.
The event handler object acts as a view observer.
To improve testability, it doesn’t create the interactors by itself, but rather depends on factories to do that.
The event handler can be tested easily.
If needed, the test doubles can be created manually.
Composing all objects together.
The becoming more and more popular VIPER architecture combines both presentation and event handling logic inside a presenter. This leads to often unwanted circular dependencies, puts two responsibilities into a single object, and may over time grow into the Massive Presenter problem. The event handling and presentation logic could be separated using The Clean Architecture event flow approach. As a result, the objects become smaller, more testable, and responsible for one thing only.