Testing user interfaces can be hard, mainly because UIs are designed to be driven by users and not code. There are various more-or-less hacky solutions for this that usually involve building some kind of robot arm to poke the UI in response to a script. The problem is that these solutions tend to be slow, and usually require a fair amount of hoop-jumping to get going.
Testing iOS user interfaces isn’t any different. There are tools like Frank and Calabash that will drive the UI with a kind of headless browser that will be familiar if you’ve done any kind of behaviour-driven Rails development (the Cucumber, Rspec and WebRat/Selenium/Capybara etc approach).
But these headless approaches suffer from all the same problems. Tests can be slow to run, the testing environment can be tricky to set up, and you need to step away from the Objective-C environment and start wrestling with Ruby and regular expressions to write the tests.
An alternative approach is to cheat slightly, and test the UI from the inside.
This builds on the model-view-controller model - in an idealised iOS app, the UI that the user touches is just a thin veneer that does two things. It displays information to the outside world through controls like UILabels; and receives user interactions through controls like
UIButtons. It’s dumb (or should be dumb) - the label contents are displayed exactly as the controller passes them into the view; and the touches are handed straight back up the responder chain for the controller to deal with.
So testing from the UI perspective involves two aspects - that display controls such as labels are displaying what they should be; and that touches trigger the correct results. Fortunately, both of these can be tested to a reasonable extent without needing to employ headless frameworks. Although it’s not completely fool-proof, it goes a long way to making sure that your user interfaces are backed up with decent tests.
Here’s a hypothetical situation, testing a minimal user interface by way of an example. We’ve got a
UIViewController subclass with an interface that consists of a button and a label. Tapping the button changes the content of the label, and I want to test this. We don’t want to have to install and configure Calabash or Frank - so how can this be done?
We’re going to split the testing into two parts - testing that the UI is correctly wired up; and then that the interaction methods do what they should. This set of tests is written using the Kiwi framework, but the approach will be similar using SenTest or another test library.
Testing the UI connections
The process of testing the UI connections involve creating an instance of the UI inside your test classes; and then verifying that everything is connected as it should be. In this case, I’m testing:
- that there’s should be a
UILabelpresent, and it should initially show
- that there should be a
UIButtonpresent, and that should be wired up to the
Here are the tests for checking the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
Each test should be largely self-explanatory with the aid of the
it description. The first few lines create an instance of the
TestViewController as a block variable, before each individual test is run. Then having created said instance, it accesses the view controller’s
view property, which forces the nib to be loaded and the controls to be instantiated and connected.
That’s a subtle - but important - point. Because we’re testing the presence of controls in the UI, we need to make sure the UI has been created (albeit that it’s done in the abstract while the tests run.)
Next, I’m testing that the view controller has actually been instantiated correctly - this doesn’t really add any value beyond the point where the first ‘real’ test passes, but it’s a useful check initially to make sure that the nib is being loaded correctly.
The first ‘real’ test checks that there is a UILabel property called
greetingLabel. If for any reason the
UILabel control is missing from the nib, or it hasn’t been connected to the corresponding
IBOutlet, then that property will be
nil and the test will fail. Similarly, it will fail if for some reason the control isn’t of the right type.
Having checked that the control exists, the next test makes sure that the default value is set up correctly. In this case, it’s ensuring that
I'm waiting... will be displayed in the UI when it first loads.
Testing the button
The tests for the button are quite similar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
The same process of instantiating the view controller gets underway, then there’s a test for the existance of the
The next test grabs the actions for the view controller from the button as an
NSArray. The name of each action will be present in the array as an
NSString, so the test is simply a case of checking that the method name exists in the array. If the button hasn’t been connected to an action, this method name won’t exist and the test will fail.
Testing the methods
Now we can test the methods themselves:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
Again, the same view controller instantiation process takes place. Then the methods are tested - firstly, ensuring that the view controller declares the updateGreeting: method.
Then, it’s time to test that the method actually does what it’s supposed to. We call the method with
[vc updateGreeting:vc.greetingButton] and test that the
greetingLabel’s text property has been updated to
Assuming all the tests go green, we’ve checked that the interface is wired up correctly, and that the appropriate methods operate correctly when they’re called. No need for a headless framework, but a suite of tests that can be repeatedly run as the project progresses.
This approach isn’t completely comprehensive - testing gestures would be tricky, for example - but it does go a long way towards automating the testing of user interfaces. This can pay off in two ways:
it reduces the risk of problems as a result of dumb misconfiguration mistakes - not connecting a
UIButtonto an action, for example. Those problems can be difficult to track down, assuming you bump into them in the first place.
it allows you to test tricky conditional logic that’s driven by the UI - for example, toggling controls on and off in response to user actions. Testing this kind of functionality manually is tedious and error-prone - by running the tests programatically, it means they’re repeatable and easy to add edge-cases to.
Adding a headless browser approach into the mix will mean you’ve got as much test coverage as it’s realistically feasible to get - but this approach can get you a long way towards that nirvana with a lot less configuration hassle.