How to Unit Test View Controllers in iOS

On September 12, 2016, Yodle hosted over 100 iOS developers at its NYC office for the September iOSoho meetup. Our Mobile Engineering Manager, Martin Rybak, presented a talk about unit testing view controllers in iOS. It is available on Youtube. Below is a transcript of that presentation:


So, tonight we’re going to talk about unit testing view controllers, something that we should all be doing more of, I think. My name is Martin Rybak, I’m the Mobile Engineering Manager here at Yodle, we’re now a part of Web.com. And what we do, we make marketing and operations software for small businesses, and the tool that I work on primarily, and my team, is called Lighthouse Field Service, and this is an operational tool for mobile businesses like contractors and landscapers. It’s operationally embedded, so it’s critical that the app works as intended, all the time, because their business depends on us. So when we started about 2 years ago, we came up with a manual test plan that had about a couple hundred test cases and took hours to run. Now fortunately we had a QA testing company help us, but we definitely knew this was not sustainable. We wanted to ship faster without fear of breaking things. Does that sound familiar? So quick show of hands, how many of you guys have any kind of automated tests at all in your apps? Alright, that’s more than half, that’s awesome. So, like this graph shows, the more automated tests you have, the fewer manual tests you need to have and you can ship faster.

So, just a quick overview of the different kinds of testing. So we just talked about unit tests. Unit tests are when you test individual components of your app independently. And they’re small, they’re fast, they’re cheap, and they’re plentiful. And they’re also a great way of documenting your components, because they show how the API is supposed to work.

There are also integration tests. Integration tests are when you test multiple components together. Those are slower, more complex, and more fragile. But they’re more realistic for simulating user workflows.

Finally, you can have your good old fashioned manual tests, where you test your app as a whole, pointed to a real backend. But they’re the slowest and most expensive, so you want to use them sparingly. So our goal was to replace manual tests as much as possible.

So here’s a quick little difference between unit tests and integration tests.

So, what did we do? Well, we wrote regular old Xcode unit tests for our non-UI classes. And then we used a tool called Appium to write integration tests for our UI. Appium is an open-source, cross-platform test runner that runs apps in a simulator, and it’s a black box test so there’s no access to your app internals. But it’s also a black box test with no access to your app internals, and that’s a problem because sometimes you want to get to a particular state, and you can’t do that without some sort of secret backdoor or pointing your app to some sort of special server. Also, if you have a screen deep inside a workflow, you have to tap through multiple screens to get there. Also, if you have a view controller that’s contained in multiple workflows, if you make a breaking change to one of them, it’s going to break all of the workflows. Now, as of Xcode 7 there’s this new XCUITest framework, that behaves very much the same way, but it’s also a black-box test; you have no access to the app internals, and you have the same problems. So for us these kinds of tests were just too slow and fragile.

So, what we wanted was a way to test a single view controller one at a time, just like we do with unit tests. We wanted to have a lot of small, specific, isolated tests that could test all the view controller’s functionality. Now we wanted two kinds of tests. First, we wanted to make sure that given a certain data input, the view controller would show a certain screen output, and then given a certain screen input, that it would show a certain data output. So, really simple. Turns out, this was totally doable. Here’s why.

As of Xcode 5, XCTest projects automatically run in the iOS simulator. You may have seen the simulator window pop up even though you don’t have UI tests. So inside a unit test, all you have to do to display a view controller is just call setRootViewController: on the UIApplication key window. And then we can use a library called KIF to find and interact with views using accessibility labels. Now KIF stands for “Keep it Functional,” it’s an open source library by our friends over at Square, and it’s really great. But you have to remember to enable the Accessibility Inspector in the simulator.

Alright, so let’s see some code! So first I’m going to show you this really simple sample app, right? So here I’ve got a very simple master-detail setup. I’ve got some items here, I don’t know who these names are, apparently they’re like, I don’t know who they are but my team made me put these in here, so shout out to my team. Alright, so this is a very simple master-detail setup, I’ve got a master view controller with various items. When I tap on an item, it takes me to a detail view controller, right? It can’t get any simpler than that. But I want to create tests around this, right? So, our specs are as follows. I want to make sure that an item name gets displayed on our master view controller. I want to make sure that tapping on an item name invokes an event that opens up a detail view controller. And also, if there happen to be no items in this list, I want it to show an empty label.

Alright, so let’s give it a shot. So here I am, I have a standard XCTestCase, and I’ve got my unit tests here, I’ve stubbed them out. They have to be prefixed with the word “test” for the Xcode test runner to find them. I’m going to write a test to make sure that the item name is visible, so the first spec that I just mentioned. So first I’m going to create an item; it’s a simple model class called YOItem. I’m going to instantiate my master view controller, passing into this init method, an array of items, I’m going to pass in the item I just created. And here is where I’m going to present the view controller in the simulator. So I’m going to call [[[UIApplication sharedApplication] keyWindow] setRootViewController:masterViewController]; That’s where the magic is, right? So that’s going to show on the screen. And then I’m going to use KIF, so I’m going to use a command in KIF called tester, this is a C preprocessor macro, I’m going to say waitForViewWithAccessibilityLabel:@”Dog”, so I’m going to make the test fail. So what this is going to do, it’s going to search the view hierarchy on the screen for a view with accessibility label of “Dog”. Now UILabels are cool because their accessibility label is actually equal to their text content, same with buttons and things like that, but if you make a custom UIView you have to set those yourself.

So if I run this test right now, it’s going to put the view controller on the screen, and it’s going to wait because it can’t find that view. And after 10 seconds, KIF is going to give up, and timeout and throw an exception. And there it is, so it says failed to find a view with that label. So I’m going to change this to say item.title. Now it’s going to look for “Foo”. If I run this again, it should pass. Woohoo, there we go. So now my next spec, I’m going to make sure that if there are no items passed, I’m going to show an empty label. So let me take the master view controller, I’m going to initialize it with an empty array. Then throw it on the screen by copying this command here. And I’m going to wait for an accessibility label, “No Items”. And then I run this test. Alright, that passed. And now I’m going to test one more thing. So, I want to make sure that…so when I select an item, the API of this view controller is designed to invoke a block property, and this block is like a callback, so it’s going to invoke onItemSelected with the selected item. So I can write a test around that. So I need to create an item and master view controller, throw it on the screen. And I’m going to use a different KIF command here called [tester tapViewWithAccessibilityLabel:item.title].

So at this point, it’s not going to do very much, right? I want to make sure that that block callback is invoked. So I’m going to go ahead and set a handler for that callback. So I’m going to say masterViewController.onItemSelected = ^(YOItem* selectedItem) {}; OK, so that’s my handler. So, this is an asynchronous invocation so to do this you have to use XCTestExpectation which is Xcode’s way of handling asynchronous tests. So I’m going to quickly do that. This is just out of the book, so XCTestExpecation* expectation = [self expectationWithDescription:@”test”], OK? And then inside this callback I’m going to fulfill that expectation. And at the end here I’m going to tell Xcode to [waitForExpectationsWithTimeout:1.0 handler:nil]; Alright, so I run this test now, you’re going to see it fire up in the simulator. It’s going to tap on that item, and invoke my callback. There it is, my test passes. But I got to test one more thing. I got to make sure that this item I got back in the callback is the same as the item that I passed in. So I’m going to do a simple XCTAssertEqual and I’m going to make sure that the selected item I just got is equal to the item I passed in. So if I run that now, it should pass as well. Alright, awesome so now I’ve got a whole bunch of little tests that are really fast and isolated and very independent. I can run them all together now as part of our test suite and you can see the simulator go through each of them one by one. That’s it.

That was the easy part. Now, the hard part is actually making your view controllers testable, right? So what make a view controller testable? And I thought about like one word to sum this up, and I was like, ok, this is the word: boundaries. Alright? You have to separate the view controller from the outside world using clearly defined inputs and outputs. And those can be in the form of properties, methods, or events, and events, and events are in the form of our usual target-action, delegation, blocks, NSNotificationCenter, or KVO.

So to make this work, we need, most importantly, testable surface area. You guys have all seen, maybe written, massive view controllers that do a ton of things, right? So it writes to NSUserDefaults, it reads from SQLite or CoreData, it makes HTTP calls. And testing is really, really difficult, and you’re like, where do I even start? And you start importing private headers that expose internals of your view controllers, and the problem is that we don’t have enough testable surface area. You can only test at the seams, right, where there is input and output boundaries, where you can set up state, and verify its effects. So, if you break up this massive view controller into various logical services, like see here are some that we use at Yodle. Alright, so we have an API service that handles talking to our API and deserializing JSON. The network service wraps Reachability. The data service that maintains the Core Data stack. Preferences service that talks to NSUserDefaults. Keychain. File service for IO, Theme service for styling, and analytics for stuff like that. So, if you do it this way, then there is a huge increase in the amount of testable surface area, and also a huge amount of reusable code that you get.

So, some tips and tricks. View controllers should not know about other view controllers. I cannot make this clear enough, alright? That’s why not I’m not a fan of storyboards, alright? Because they have segues and you have know about destination view controllers and you have to cast them, and that’s bad because you can’t reuse them when you tie them like that. So they should be totally isolated and independent. To do that, move your presentation logic to a separate router object, or some sort of other object that instantiates view controllers and presents them. Don’t call out to global code like singletons, even innocent looking ones like [NSUserDefaults standardUserDefaults], because it’s hard to mock. Actually with OCMock you can actually mock class methods but with most other languages that’s not possible. So you’re better off using dependency injection to inject instances you can access. So we use a library called BloodMagic, we’ve actually contributed to it, I highly recommend it, try it out. Also, use a mocking framework, it makes it really easy to set up your inputs and verify outputs in your tests, so we use OCMock. Finally, really important, use a build server to enforce your unit tests, ideally before any build goes out the door, so your developers can’t selectively choose to ignore them. But yeah, that’s the idea.

So, gotchas, so what did we learn as we did this, right? So, we realized that it’s a best practice to create a separate testing app delegate, because sometimes your app delegate does crazy things, right? It sets up Crashlytics, it does all these crazy things, and it’s just going to mess up your tests. So, you can create a testing app delegate by going to your main.m file and doing something funny and just selectively picking which class you want to register as your app delegate. And, I have a test project which you’ll be able to access and can see how I did that. They’re also fast, but they’re not that fast, right? So you saw those tests, they took a couple seconds, but when you have hundreds of them, I mean you’re talking serious time here, we’re talking 15–20 minutes. So, we are currently looking into ways we can speed that up by parallelizing them or disabling animations, or changing the CALayer animation speed or something like that, so just keep an eye out for that. Retain cycles, if you hate them now, you’re really going to hate them because what’s going to happen is, a bunch of view controllers are going to be in memory, alive, and if they’re listening to the same notification, they’re going to go crazy. So, definitely watch out for those. Also, remember to dismiss any UIWindows that you create, alright, so if you create a UIAlertController, UIAlertView or action sheet, right, those are new UIWindows. Or if you create any custom ones yourself. You have to dismiss those manually, otherwise they’re going to stay in memory and do weird things. Finally, find ways to not repeat yourself by subclassing XCTest like we did, or adding some methods to KIF to make your lives easier, there’s no reason to repeat yourself.

So finally, tests are only as good as you make them. They seem time consuming at first, but they definitely pay if in terms of faster release cycles and just peace of mind, so I highly encourage them. So, that’s it for my talk. That’s the repo with some sample code, and that’s my Twitter handle, email, and we are hiring! So we’re looking for a mid to senior developer. Do you want to join us and do fun things with testing for small business who really count on you? Come see me.

Question: So everything is in Objective-C. Are you using Swift at all, and if so, how do you handle OCMock that doesn’t work with Swift itself?

So, the question is do we use Swift, and if we do, how do we handle things like OCMock? Well, we don’t use Swift precisely because of that reason. I’m still waiting for proper support for things like that, for mocking and things, I think that’s a critical part of our infrastructure and we can’t just give it up because of a shiny new language. So as much as we’d like to, you know, I have to wait for that.

Question: You talk about view controllers shouldn’t know about other view controllers and you handle that with a presentation layer. Could you elaborate on that?

Yeah, so I can actually show you what it looks like. So, we call ours a coordinator class. So this is what it looks like. Well, we should start from the beginning. So the app delegate, all it does is fire up an instance of the coordinator and hold a strong pointer to it and then it calls launch on it. And launch is responsible for setting up whatever that coordinator does. So, it owns a navigation controller, right, and when it launches it creates a view controller and sets it as the root view controller. So here it is, here’s our master view controller, and to set off that onItemSelected what I do here is I instantiate a new detail view controller and then push it. I’ve got a couple navigation helper methods down here that find my top navigation controller that’s on the screen, because I could have many pushed on top of each other, and I push the view controller on that one.

Question: Have you tried snapshot testing?

Yeah, that’s a great question. So snapshot testing, it’s a great observation, so it’s another kind of testing that you can do. I find them to be very very brittle, right, because they go by pixels, right, so if you make the slightest change or if something happens, it will break your tests. We’re looking for more functionality, we want to make sure that the content that we care about is actually there. The fact that it’s a little pixel off, I don’t really care, I want to make sure that the content’s there. So snapshot testing is an option, we don’t currently do it, but I could totally see if it’s important for your app to do as well.

Question: How much preparation do you find you have to do in setup and teardown for injecting into your view controllers. Because maybe your app has a core data stack or a network layer.

That’s a good question. So, in our setup method we do create an instance of our data stack, alright so our data service, and we’ll create an in-memory representation only, so there’s no database behind it. So must of our tests do have that kind of available, always in memory, and we do other things too like the theme service to make things look good. We do kind of keep around a couple of core ones every time.

Question: How stable have you find KIF to be, because XCUITest for example, and even worse, Appium, there’s all kind of waits you have to have to put in. Is KIF really good or do you have all kind of weird things that you have to do?

Clearly, you have a lot of experience with this, because it is maddening. Yes, we’ve have had to make some waitForTimeInterval: type commands or waitForAnimationsToFinish. It’s definitely not perfect, but it’s a hell of a lot better than what we had before, which was nothing. So, I have heard about Google’s new EarlGrey library…this guy says it’s good, believe him. We’re looking into exploring that. So KIF is definitely not without its problems, for sure, so definitely look into other libraries or things that do something similar. I mean, we do have flaky tests, right now we’re struggling with fixing a bunch of tests that are breaking for no reason and we don’t know what it is, so, you know it takes time, but once they do get working, you feel really good knowing that, ok, the stuff that’s important is really under control.

Question: Do you have a sense for how many tests you have in your app as well as how long they take?

Mark: 1,427. This is Mark. Yeah, we have 1,400 tests, they’re not all UI tests, and it takes about 20 minutes to run, 30. That’s my concern actually, is that the more tests we have it it gets longer and longer and longer, and it’s starting to become kind of like, come on, half an hour is really long, so we are looking to ways to parallelize that or just to shorten that. There are some tricks apparently that we could do with CALayer speed and things like that, so we’ll check those out.

Question: Do you always wait until all the tests pass until you merge into master?

Yes, so it’s our policy. So generally for a feature we’ll have an integration branch and lots of little branches that go into it, and then that integration branch, our Bamboo server, it sees the word integration in the branch name, it knows to run tests, and we don’t merge it as a policy for PR’s, we won’t merge it until tests pass.

This article was originally posted on the Yodle Tech Blog