UIView providers for Swift unit testing

As you might already know, I deliberately chose not to use storyboards for my Swift code. Instead, I build all of the views programmatically and inject them through initializers into suitable controllers. Although this approach works really nice, it has drawbacks when it comes to unit testing.

Imagine having a user profile view implemented in vanilla UIKit. A really basic one: a picture, a full name label, a single text view for some notes and a switch which manages notification settings. Does it sound complicated? Not at all. Now, add a label for every field and layout fields vertically. You will get UserPictureView, UserNameView, UserNotesView and UserNotificationsView classes. Each one will get at least two dependencies: field's label and control (view) to manage the actual data. All of them will be grouped together in a parent view which, in turn, will be framed inside a scroll view.

The view (let's call it UserProfileView) is still pretty basic, but the number of lines of code needed to initialize it has grown rapidly. The top of the view controller's test case is occupied by dependency initialization and you have to scroll down to see the actual test definitions. Here are a couple ideas to fix that:

Refactor to a helper view class with all the dependencies pre-initialized.

Quit using initializer injection for views and make them aware of the initialization code.

Import & attach view's factory method from the target app.

None of the presented solutions is good. First of all, they require building a full view to test a view controller. Moreover, the first one violates DRY[1] (the helper class is almost the same as a full view created by a factory method) and the second one is against SRP[2] (view manages both initialization and layout). There must be a better solution...

View controller awareness

To find out a root cause of our problem, let's take a few steps back to the example mentioned in the introduction:

Despite using the same set of subviews in both cases, the latter is much more manageable. Only meaningful views that carry business logic behaviours are published in the interface. Let's try to do the same for a programmatically built view:

That is an interesting effort. It has all the benefits of the storyboard approach. Moreover, it says no to optional chaining dance and is nicely separated as a dependency, not cluttering UIViewController itself. The problem is that it is not explicitly a UIView, so it cannot be assigned to a var view: UIView directly. Let's try to come up with a work-around:

That is a terrible design. We have specified that UserProfileViewController relies on UserProfileViewType dependency, but the implementation secretly expects it to be a UIView subclass. This should never be the case in a real codebase.

Since UserProfileViewType provides the UserProfileViewController with meaningful subviews, what prevents it from delivering the whole view? Absolutely nothing. Let's rename it to UserProfileViewProvider then!

The trick with that design is that we erase the concrete UIView type. We made UserProfileViewController aware of the fact that there is a view to manage, which provides five meaningful subviews. The controller does not know (and does not care) how the view is built and what is the layout.

The biggest win in that architecture is testability. Unless subview-related business logic (?) needs to be tested, the stub is so easy to build:

Despite being a really lightweight implementation, UserProfileViewProviderMock allows testing behaviours the same way a full instance of UserProfileView would.

Flow controller / coordinator pattern

Providers play nicely with UIViewControllers too. Imagine the app is using flow controller or coordinators architecture. The flow controller's responsibility is to resolve a full instance of a view controller, configure it and push onto the navigation stack. It works seamlessly with storyboards because the storyboards are like factories of UIViewControllers. Unfortunately, it is not the case when manually building view controllers. The flow controller has to rely on a factory method which returns a complete instance of a concrete view controller. Again, this is not that great for the sake of unit tests.

Fortunately, you can define a provider for a UIViewController like this:

Now, the view controller has even less information about the view's implementation. It expects a single component that can publish and receive text updates, but it does not know whether it is a UITextField or UITextView. With such an approach it is possible to write unit tests for controllers without creating UIKit controls at all!

Summary

Providers are like the adapters designed specifically to wrap around UIKit library. They allow to "type-erase" views or view controllers to make them easily instantiable in unit tests. Personally, I find this is the clearest approach to testing controllers (and flow controllers) I have come up with so far. You should definitely check providers out!