Testing Apples MVC
The designpattern we all use in one way or the other
Apple’s official main design pattern for iOS-apps is the Model View Controller. This pattern contains a model (which contains our data), a view (which displays the data) and a controller (that kind of glues everything together). From their point of view, everything can be classified as part of one of this three components. So let’s see how we can test every single component.
The Model contains the data needed to work. But be careful; it does not include any data that is only relevant to the view, such as the text a button displays. Instead it might be a Core Data storage, or a game system.
This means we already know how to test models. Models expose an api (methods, properties, …) and they do not require any extra work to set up tests for.
For the view we have to assume that Apple takes care of converting the physical interaction into data and transferring it to the correct position of the screen/view. With this assumption in place we can create two kinds of view data we want to test:
- Changing parts
We could test the static parts by creating a screenshot of how it’s supposed to look and comparing a current screenshot with this, but in all honesty, this is something for UX. As developers I think we’re okay if we just concentrate on the changing parts and interactions of a view. Interestingly enough, this results in us having to test the controller, since it is conveying the interactions.
Let’s start working on the glue of an app. In iOS it’s called the ViewController. This controller accepts events, controls the logic and makes our app work. All this functionality leaves a “hard to test” impression. At least it did on me. It gets even stronger if you consider that iOS has two different types of creating views with the InterfaceBuilder. So why does it appear to be difficult to test?
- It does a lot, which means a lot of testing and a lot of interacting parts.
- How do I test methods of the view lifecycle?
- View-specific methods, such as tableView(tableView: didSelectRowAt:)
- How do I test IBActions?
- How do I test IBOutlets?
Lets tackle every one of these points.
It does a lot
Sounds bad… and it is. In most cases you want to consider refactoring your code. If you can’t reduce the amount of functionality in the code, it still doesn’t stop you from testing. It’s just scary in the beginning and you might want to refactor on the way.
How do I test methods of the view lifecycle?
There are two opinions. One is that you call them the same way you would every other action. As long as you don’t have any code referencing IBOutlets, you might even be able to go through the whole life cycle yourself.
The other option is to extract all the code out of these functions and just call them. This way you can always make sure, small logical code fragments do as they are supposed to. So this is the same thing we did everywhere else.
Sounds odd, but in the end, they are only functions. In this case they are public, so you don’t have to create a category and add them.
IBActions and IBOutlets
IBActions are just methods. Call them!
IBOutlets on the other hand need the ViewController to inflate the view and set up all the Outlets. As soon as that is done, you can test the same way you always would with properties. So the question is: how do I inflate a view? Without further ado…
Loading ViewController in Unit-Tests
Apple is nice. They give us two ways of creating views in the InterfaceBuilder. But that also creates the necessity of being able to test both kinds of views. Xibs are the old way Apple created views. You had a view and loaded it via:
testViewController = TestViewController
(nibName:"OfferDetailViewController", bundle: nil)
_ = testViewController.view
The little property "view" starts the entire View-Life-Cycle, thus also setting up your IBOutlets.
Okay, some people might say Xibs are the old way (I know some programmers that disagree here) how about storyboards? This is a little bit trickier. You have to load the storyboard and than load the view. What makes it even trickier is that you have to enter a Storyboard ID for your ViewController. Otherwise you won’t be able to find it in your storyboard.
So what does the code look like to inflate a storyboard view?
let storyboard = UIStoryboard(name: "MyStoryboardName", bundle: nil)
let testVC = storyboard.instantiateViewController(withIdentifier: "TestViewController")
_ = testVC.view //starting with iOS 8 this is also necessary for storyboards
We might not use MVC as the architecture in our app, but it is still a nice to start thinking about testability of different components. They tend to be similar and the same problems arise for the UI as they do in MVC.
Next: What about Networking
Previous: Unit-Tests in Swift