Historically, there hasn’t been a lot of testing done on iOS, and we see the results of that every day—regressions, crashes, consistently reproducible failures. As we build more complex applications, manual testing takes more and more time and automated testing becomes increasingly necessary.
The biggest challenge of testing on iOS starts at the UIViewController with the tangling of view and control code. Sometimes it continues all the way down, and model and database code also becomes entwined. Testing on iOS is a vicious cycle – the architecture patterns are hard to test, so we don’t test, so we don’t consider testability in our architecture, so things become even harder to test.
The first step is to break this cycle, which starts with breaking up the UIViewController. There’s a Reactive Cocoa pattern called MVVM but regardless of the approach we take, we need to get that control code out of the UIViewController, then we can test them separately. I’ve outlined a strategy for that in more detail here.
This might seem a bit overwhelming, so a good place to start when writing unit tests is the class with the least complexity and the fewest dependencies. Typically this is our model classes. When it comes to model classes it can be tempting to think that this is so simple that there’s no need to test it. But I think it’s still worthwhile because:
- How would we decide when something is “complicated” enough to test?
- What if it later became more complicated?
- If it were that easy, the test would be very quick to write.
- We want to be confident this piece works well, so that we can rely on it in larger tests later.
As we get to more complicated objects we need to master the art of Dependency Injection. This is when we pass an argument in the initialiser, rather than creating it in the class. Dependency injection helps make our code more testable by clarifying dependent objects that we can replace with mocks or otherwise control or observe.
Unit-Testing UI Code
As mentioned above, it’s challenging to test UI code on iOS because of that entangling of control and view code. Let’s talk about one example: testing buttons.
Typically, when testing buttons, we don’t actually test the buttons—we test that the methods that are added to the buttons work as they should. There are two downsides to this.
Firstly, we have to expose a method for testing. Not that big a deal—this is why Java has the @VisibleForTesting annotation. But we might prefer not to do that. In Objective-C we can also declare a category in our test class to allow our tests to “see” the private method, but I like this strategy even less.
Secondly—and most importantly—this doesn’t actually test what happens when the button is tapped. What if the wrong @selector is set? Or if we autocomplete to UIControlEventTouchUpOutside instead of UIControlEventTouchUpInside?
Alternative: we write tests to return real UIButtons, then we can tap it and verify what happens. I’ve covered how in more detail here.
UIAutomation tests are also known as monkey tests—something that will go through and tap buttons on your app and make things happen. These are proper “black box” tests—all they know about is the UI, not the inner workings of your code.
UIAutomation tests are integration tests, rather than unit tests, so it’s a complementary testing strategy to writing extensive unit tests. While unit tests check the internal workings of your app, UIAutomation tests are a good way to check the flow of the app—that each view controller loads, for example.
UIAutomation tests are:
- Slower to run.
- Hard to test corner cases on.
UIAutomation tests are great for:
- Showing that the things you built are glued together correctly.
- Making sure your app is accessible throughout.
- Checking that each view loads.
- Testing things like carousels.
If you’re interested in learning about testing iOS code in more detail, I have a workshop with sample app that covers these strategies in more detail.