UIView providers for Swift unit testing

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:

  1. Refactor to a helper view class with all the dependencies pre-initialized.
  2. Quit using initializer injection for views and make them aware of the initialization code.
  3. 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:

class UserPictureView: UIView {
  let label: UILabel
  let pictureView: UIImageView

  init(label: UILabel,
       pictureView: UIImageView) {
    // ... layout
  }
}

class UserNameView: UIView {
  let label: UILabel
  let nameLabel: UILabel

  init(label: UILabel,
       nameLabel: UILabel) {
    // ... layout
  }
}

class UserNotesView: UIView {
  let label: UILabel
  let notesTextView: UITextView

  init(label: UILabel,
       notesTextView: UITextView) {
    // ... layout
  }
}

class UserNotificationsView: UIView {
  let label: UILabel
  let notificationsSwitch: UISwitch

  init(label: UILabel,
       notificationsSwitch: UISwitch) {
    // ... layout
  }
}

class UserProfileView: UIView {
  let scrollView: UIScrollView
  let scrollViewContainer: UIView
  let userPictureView: UserPictureView
  let userNameView: UserNameView
  let userNotesView: UserNotesView
  let userNotificationsView: UserNotificationsView

  init(scrollView: UIScrollView, 
       scrollViewContainer: UIView,
       userPictureView: UserPictureView,
       userNameView: UserNameView,
       userNotesView: UserNotesView,
       userNotificationsView: UserNotificationsView) {
    // ... layout
  }
}

class UserProfileViewController: UIViewController {
  let userProfileView: UserProfileView
 
  init(userProfileView: UserProfileView) {
    // ... make userProfileView the root view
  }
}

Now, let's try to answer a simple question. What exactly does it mean that UserProfileViewController relies on UserProfileView dependency? The view controller:

  1. Is fully aware of the subview hierarchy.
  2. Knows everything about the layout (whether the labels are positioned to the left or to the right).
  3. Is aware of auto-layout catches (scroll view content has to be put inside a container view).

This is by far too much of a knowledge for that controller! Compare it to a Storyboard example:

class UserProfileViewController: UIViewController {
  @IBOutlet weak var scrollView: UIScrollView?
  @IBOutlet weak var userPictureView: UIImageView?
  @IBOutlet weak var userNameLabel: UILabel?
  @IBOutlet weak var userNotesTextView: UITextView?
  @IBOutlet weak var userNotificationsSwitch: UISwitch?

  // ...
}

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:

protocol UserProfileViewType: class {
  var scrollView: UIScrollView { get }
  var userPictureView: UIImageView { get }
  var userNameLabel: UILabel { get }
  var userNotesTextView: UITextView { get }
  var userNotificationsSwitch: UISwitch { get }
}

class UserProfileView: UIView, UserProfileViewType {
  // ... 

  // MARK: - UserProfileViewType

  var scrollView: UIScrollView {
    return self.scrollView
  }

  var userPictureView: UIImageView {
    return self.userPictureView.pictureView
  }

  var userNameLabel: UILabel {
    return self.userNameView.nameLabel
  }

  var userNotesTextView: UITextView {
    return self.userNotesView.notesTextView
  }

  var userNotificationsSwitch: UISwitch { 
    return self.userNotificationsView.notificationsSwitch
  }
}

class UserProfileViewController: UIViewController {
  let userProfileView: UserProfileViewType

  init(userProfileView: UserProfileViewType) {
    self.userProfileView = userProfileView
    // ... 
  }
}

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:

class UserProfileViewController: UIViewController {
  override func loadView() {
    // Don't try this at home!
    guard let profileView = self.userProfileView as? UIView else {
      fatalError("UserProfileView should be a UIView subclass")
    }
    
    self.view = profileView
  }
}

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!

protocol UserProfileViewProvider: class {
  var view: UIView { get }

  var scrollView: UIScrollView { get }
  var userPictureView: UIImageView { get }
  var userNameLabel: UILabel { get }
  var userNotesTextView: UITextView { get }
  var userNotificationsSwitch: UISwitch { get }
}

class UserProfileView: UIView, UserProfileViewProvider {
  // ... 
  // MARK: - UserProfileViewProvider

  var view: UIView {
    return self
  }

  var scrollView: UIScrollView {
    return self.scrollView
  }

  var userPictureView: UIImageView {
    return self.userPictureView.pictureView
  }

  var userNameLabel: UILabel {
    return self.userNameView.nameLabel
  }

  var userNotesTextView: UITextView {
    return self.userNotesView.notesTextView
  }

  var userNotificationsSwitch: UISwitch { 
    return self.userNotificationsView.notificationsSwitch
  }
}

class UserProfileViewController: UIViewController {
  let profileViewProvider: UserProfileViewProvider

  init(profileViewProvider: UserProfileViewProvider) {
    self.profileViewProvider = profileViewProvider
    // ... 
  }

  override func loadView() {
    self.view = self.profileViewProvider.view
  }
}

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:

class UserProfileViewProviderMock: UserProfileViewProvider {
  let view = UIView()
  let scrollView: UIScrollView()
  let userPictureView = UIImageView()
  let userNameLabel = UILabel()
  let userNotesTextView = UITextView()
  let userNotificationsSwitch = UISwitch()
}

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:

protocol UserProfileViewControllerProvider: class {
  var viewController: UIViewController { get }

  var onProfilePictureTapped: ((UIImage) -> Void)? { get set }
}

Again, this erases concrete type of a UIViewController and provides only properties / methods that are relevant to flow management.

Providers in MMVM

UIView providers play really well with MVVM. Instead of providing a concrete UI component, the view provider can return behavioral abstraction of such a component. Let's check this RxSwift example:

protocol UserNotesViewProvider: class {
  var view: UIView { get }
 
  var notes: ControlProperty<String> { get }
}

class UserNotesView: UIView, UserNotesViewProvider {
  // ...
  // MARK: - UserNotesViewProvider
  
  var view: UIView {
    return self
  }

  var notes: ControlProperty<String> {
    return self.aTextView.rx.text
  }
}

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!

class UserNotesViewProviderMock: UserNotesProvider {
  let view = UIView()

  let notes: ControlProperty<String>
  let notesVariable = Variable<String>("")
  let notesObserver = LastValueObserver<String>()

  init() {
    self.notes = ControlProperty(
      values: self.notesVariable.asObservable(),
      valueSink: self.notesObserver)
  } 
}

class LastValueObserver<T>: ObserverType {
    var lastValue: T?
    
    public func on(_ event: RxSwift.Event<T>) {
        guard let element = event.element else {
            return
        }
        
        self.lastValue = element
    }
}

// Inside the test case:
let notesProvider = UserNotesViewProviderMock()
// invoke a note change to test what happens
notesProvider.notesVariable.value = "New note"
// check the last published note is fine
notesProvider.notesObserver.lastValue == "Expected"

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!


  1. Don't-Repeat-Yourself principle states that the logic duplication should be eliminated with an abstraction. ↩︎

  2. Single Responsibility Principle states that every module / class should be responsible for a single functionality provided by the software. ↩︎