Users expect a high level of polish from iOS apps, so it’s up to you to design, develop and test your apps to meet ever-rising expectations. Think about it for just a moment: How much time have you poured into conducting basic manual user interface testing? You know the drill…launching your app from Xcode, and incessantly tapping the same series of buttons to make sure no regressions slipped into your design. Surely, there are other things you’d rather do?
Instead, consider that the enhanced test UI in Xcode 5 and continuous integration support in OS X Server demonstrate Apple’s commitment to provide developers the best tools. That’s great, you might say, but how do you automate testing simple user actions, like ensuring a double-tap or swipe at the right spot brings up the correct view? Even test scripts and bots don’t have capacitive touch fingers to swipe across the screen…or…do they?
In this tutorial, you’ll learn all about KIF (“Keep it Functional”), an open-source user interface testing framework. With KIF, and by leveraging the Accessibility APIs in iOS, you’ll be able to write tests that simulate user input, like touches, swipes and text entry. These tests give your apps an automated, real-world user interface workout, and help keep your mind at ease so you can just focus on developing that killer app – not spending half a lifetime on UI testing.
Let’s get testing!
Getting Started
The sample project is a timer app called Solanum (named after the tomato plant species) based on the Pomodoro time-boxing method. Here’s how it works: you work for a defined number of minutes, take a break, then repeat. After several of these cycles, you take a longer break. The app is a just a simple timer that keeps track of time periods. Feel free to use the app afterwards to help make you more productive during your own development!
Download and unzip the starter project archive here. Note that KIF is a separate project, and its role is to build a library for Solanum’s test target. You’ll need to double-click solanum.xcworkspace to open the project in Xcode rather than solanum.xcodeproj. Look for two projects to open in the in the project navigator, it should like the example below.
Set the app target to solanum, and select either the 3.5-inch or 4-inch iPhone Simulator target. Don’t use the 64-bit build; at the time of writing this tutorial KIF didn’t appear to be fully compatible. Build and run the app. Look around, then switch to the Settings tab.
The app has a debug mode that accelerates time, so you can set a 20-minute timer and it will help with testing by ticking by in 10 seconds. This is just to aid testing the app. You wouldn’t want each test run to take 20 minutes!
Turn on Debug Mode switch to speed up the timer. Next, tap the Clear History button, and then tap Clear on the confirmation alert view. These steps will ensure you’re starting out in a clean, test-friendly environment. Return to Xcode and stop the app.
Pre-Test Actions
In the Project Navigator, expand the solanum project. Right-click the UI Tests folder and click New File… to add your first test case.
Select iOS\Cocoa Touch\Objective-C class and click Next. Name the class UITests
and make it a subclass of KIFTestCase
.
Click Next and make sure the files are be added to the UI Tests target, not the solanum target. Finally, click Create on the following screen to save the files.
KIFTestCase
is a subclass of SenTestCase
. That means you have most of the standard OCUnit testing methods and mechanisms available, in case you’re already familiar with unit testing.
Open UITests.m and add the following method after the @implementation
line:
- (void)beforeAll { [tester tapViewWithAccessibilityLabel:@"Settings"]; [tester setOn:YES forSwitchWithAccessibilityLabel:@"Debug Mode"]; [tester tapViewWithAccessibilityLabel:@"Clear History"]; [tester tapViewWithAccessibilityLabel:@"Clear"]; } |
beforeAll
is a special method that is called exactly once, before all of the tests run. You can set up any instance variables or initial conditions for the rest of your tests here.
The tester
object is a special shortcut to an instance of the KIFUITestActor
class. That class includes the methods that will simulate user activity, including tapping and swiping on views.
tapViewWithAccessibilityLabel:
might be the most common test action method. As the name suggests, it simulates tapping the view with the given accessibility label. In most cases, such as for buttons, the accessibility label matches the visible text label. If not, as you’ll see in the next section, you’ll need to set the accessibility label manually.
Some controls, such as UISwitch
, are more complicated and need more than just simple taps. KIF provides a specific setOn:forSwitchWithAccessibilityLabel:
method to change the state of a switch.
In summary, this method has four steps for the test actor:
- Tap the “Settings” tab bar button.
- Set the “Debug Mode” switch to its “On” state.
- Tap the “Clear History” button.
- Tap the “Clear” button on the UIAlertView.
Do these steps seem familiar? They should be! They’re what you did manually in the previous section!
Run the tests by going to Product\Test or hitting Command-U on the keyboard. You should see the app launch; then you’ll see KIF take over, automatically enable debug mode and clear the history.
If you have notifications enabled, Xcode will also tell you the test status:
Sometimes the test runner, or KIF, can get a little finicky and will refuse to run your tests, in which case you’ll just see a blank simulator screen. If this happens:
- Clean the project (Product\Clean)
- Build and run
- Wait for the app to launch
- Stop the app in Xcode
This process ensures the Simulator is running and that you’re working with the latest build. After going through the above steps, try running the tests again. The problems should be gone.
If you continue to have trouble, check out the KIF troubleshooting steps.
Now that you have a pre-test action in beforeAll
, it’s time to add your first test!
A Simple Test: Tapping Around
The app has a standard tab bar controller layout with a UINavigationController
inside each of the three tabs. To warm up for the next exercise, you’ll determine if:
- The storyboard is wired up properly
- The tabs display the correct views.
Tab bar buttons automatically set accessibility labels to be the same as their text label, so KIF will be able to find the History, Timer, and Settings tab bar buttons with the labels “History”, “Timer”, and “Settings”.
The History tab has a table view that shows all the tasks performed with the timer. Open HistoryViewController.m from the solanum group and add these lines to the end of viewDidLoad
:
[self.tableView setAccessibilityLabel:@"History List"]; [self.tableView setIsAccessibilityElement:YES]; |
This will set the table view’s accessibility label, so that KIF can find it. Usually, a table view is only accessible if it’s empty. If there are table view cells they’re a more likely target, so the table view itself will hide from the accessibility API. Essentially, the accessibility API assumes, by default, that the table view isn’t important. This is likely the case in terms of accessibility, but if you want to reference the table view in KIF then it needs to be accessible as well. The setIsAccessibilityElement:
call ensures the table view is always accessible, regardless of its contents.
Depending on the app, accessible non-empty table views can actually make things more difficult if for users who use the accessibility features (e.g. VoiceOver). In your own apps, you can wrap lines of code between #ifdef DEBUG
and #endif
directives so they’re only compiled into your debug builds. The DEBUG
preprocessor macro is pre-defined in Xcode’s project templates.
The Timer tab has several controls you could look for, but the “Task Name” text field is conveniently at the top of the view. Rather than set the label through code, open Main.storyboard and find the Timer View Controller view. Select the task name text field.
Open the Utilities panel if it isn’t already up — and select the Identity Inspector. Hint: it’s the third icon from the left, or use the keyboard shortcut ‘⌥ ⌘ 3′.
Under Accessibility in the inspector, enter “Task Name” in the Label field. Stay sharp now, because accessibility labels are case-sensitive. Be sure to enter that exactly as shown with a capital T and N!
The Settings tab has already been set up the views with accessibility labels, so you’re all set to move to the next step!
In your own projects, you’ll need to continue to fill in the accessibility labels from code or in Interface Builder as you’ve done here. For your convenience, the rest of sample app’s accessibility labels are already set.
Back in UITests.m, add this method after beforeAll
:
- (void)test00TabBarButtons { // 1 [tester tapViewWithAccessibilityLabel:@"History"]; [tester waitForViewWithAccessibilityLabel:@"History List"]; // 2 [tester tapViewWithAccessibilityLabel:@"Timer"]; [tester waitForViewWithAccessibilityLabel:@"Task Name"]; // 3 [tester tapViewWithAccessibilityLabel:@"Settings"]; [tester waitForViewWithAccessibilityLabel:@"Debug Mode"]; } |
The test runner will look for all methods that start with the word “test” at runtime, and then run them in alphabetical order. This method starts with the name “test00″ so that it will run before the tests you’ll add later, because those names will start with “test10″, “test20″, etc.
Each of the three parts of the method will perform a similar set of actions: tap on a tab bar button, and check for the expected view to show on the screen. 10 seconds is the default timeout for waitForViewWithAccessibilityLabel:
. If the view with the specified accessibility label doesn’t show itself during that timeframe, the test will fail.
Run the tests by going to Product\Test or hitting Command-U. You’ll see the beforeAll
steps which will clear the History, and then test00TabBarButtons
will take over and switch to the History, Timer and Settings tabs in sequence.
Well, what do you know? You just wrote and ran an interface test, and saw your little app “drive” itself! Congrats! You’re on your way to mastering automated UI testing.
User Input
Sure, switching tabs is nifty, but it’s time to move on to more realistic actions: entering text, triggering modal dialogs and selecting a table view row.
The test app has some built-in presets that will change the work time, break time and number of repetitions to a set of recommended values. If you’re curious to see their definitions, have a look at presetItems
in PresetsViewController.m.
Selecting a preset could be a test of its own, but that action is more efficient when it is a part of other tests. In this case, it’s worth splitting it out into a helper method.
Add the following method to the implementation block of UITests.m:
- (void)selectPresetAtIndex:(NSInteger)index { [tester tapViewWithAccessibilityLabel:@"Timer"]; [tester tapViewWithAccessibilityLabel:@"Presets"]; [tester tapRowInTableViewWithAccessibilityLabel:@"Presets List" atIndexPath:[NSIndexPath indexPathForRow:index inSection:0]]; [tester waitForAbsenceOfViewWithAccessibilityLabel:@"Presets List"]; } |
The first step here is to switch to the Timer tab so that you’re in the right place. Then tap the Presets button in the navigation bar. When the “Presets List” table view appears, tap on the row at the specified index.
Tapping on the row will dismiss the view controller, so use waitForAbsenceOfViewWithAccessibilityLabel:
to ensure it vanishes before you continue.
Did you notice that this method doesn’t start with the word test? The test runner won’t automatically run it. Instead, you’ll manually call the method from within your own tests.
Now, add the following test method to UITests.m:
- (void)test10PresetTimer { // 1 [tester tapViewWithAccessibilityLabel:@"Timer"]; // 2 [tester enterText:@"Set up a test" intoViewWithAccessibilityLabel:@"Task Name"]; [tester tapViewWithAccessibilityLabel:@"done"]; // 3 [self selectPresetAtIndex:1]; // 4 UISlider *slider = (UISlider *)[tester waitForViewWithAccessibilityLabel:@"Work Time Slider"]; STAssertEqualsWithAccuracy([slider value], 15.0f, 0.1, @"Work time slider was not set!"); } |
KIF test actions have very readable names; see if you can figure out what’s going on here. Think of it as a…test! Yes, of course the pun is intentional. :]
OK, here’s what’s happening section by section:
- Switch to the Timer tab.
- Enter “Set up a test” into the text field with the “Task Name” accessibility label (remember, you added this label to the storyboard earlier). Tap the “Done” button to close the keyboard.
- Call the helper method to select the second preset.
- Selecting the preset should change the slider values, so make sure it has indeed changed to the correct value.
In the final section of code, you’ll find a handy trick: waitForViewWithAccessibilityLabel:
. Not only will it wait for the view to appear, but it actually returns a pointer to the view itself! Here, you cast the return value to UISlider
to match up the proper type.
Since KIF test cases are also regular OCUnit test cases, you can call the standard STAssert
assertion macros. Assertions are run-time checks that cause the test to fail if some condition isn’t met. The simplest assertion is STAssertTrue
, which will pass if the parameter passed in is true.
STAssertEquals
will check that the first two parameters are equal. The slider value is a float
, so be careful about matching up types. Thus, the 15.0f
appears in the assertion. You also need to be careful about small inaccuracies in floating point representations. This is because floating point values cannot necessarily be stored 100% accurately. 15.0 might end up actually being stored as 15.000000000000001 for example. So STAssertEqualsWithAccuracy
is a better choice; its third parameter is the allowed variance. In this case, if the values are within +/- 0.1 of each other, the assertion will still pass.
Run the tests using Command-U. You should now see three sequences: beforeAll
clears the history, test00TabBarButtons
switches through each tab, and then your latest masterpiece in test10PresetTimer
will enter a task name and select a preset.
Another successful test! At this point, your test mimics users by tapping all kind of things and even typing on the keyboard, but there’s even more to come!
Time to Start the Timer
Here’s an example timer cycle a user of the app might choose: work for 8 minutes, take a 2 minute break, work for 8 minutes, take a 2 minute break, then work a final 8 minutes. At this point, you take a longer break and then restart the app when you’re ready.
The parameters for the above example are:
- Work time: 8 minutes
- Break time: 2 minutes
- Repetitions: 3
The next KIF test will enter these parameters, and then tap the “Start Working” button to start the timer. Add the following method to UITests.m, directly below the previous tests you added:
- (void)test20StartTimerAndWaitForFinish { [tester tapViewWithAccessibilityLabel:@"Timer"]; [tester clearTextFromAndThenEnterText:@"Test the timer" intoViewWithAccessibilityLabel:@"Task Name"]; [tester tapViewWithAccessibilityLabel:@"done"]; [tester setValue:1 forSliderWithAccessibilityLabel:@"Work Time Slider"]; [tester setValue:50 forSliderWithAccessibilityLabel:@"Work Time Slider"]; [tester setValue:1 forSliderWithAccessibilityLabel:@"Work Time Slider"]; [tester setValue:8 forSliderWithAccessibilityLabel:@"Work Time Slider"]; [tester setValue:1 forSliderWithAccessibilityLabel:@"Break Time Slider"]; [tester setValue:25 forSliderWithAccessibilityLabel:@"Break Time Slider"]; [tester setValue:2 forSliderWithAccessibilityLabel:@"Break Time Slider"]; } |
Because this test will run immediately after test10PresetTimer
(which sets the task name), you can use the clearTextFromAndThenEnterText:intoViewWithAccessibilityLabel:
variant rather than plain old enterText:intoViewWithAccessibilityLabel:
to clear out any existing text first.
Finally, there are several calls to setValue:forSliderWithAccessibilityLabel:
. This is the UISlider
specific method to set the new value. Note that the accuracy isn’t always very good. KIF actually simulates the touch event and swipes to set the new value; sometimes the pixel calculations are a little off. But that’s okay, because fingers aren’t all that accurate either!
You only need to set each slider’s value once. The multiple calls are just for kicks, and so you can see KIF changing the value over and over.
Run the tests using Command-U.
The remaining things to test in the UI are the UIStepper
control to set the number of repetitions, and the “Start Working” button. The “Start Working” button is easy – you can use tapViewWithAccessibilityLabel:
to simulate a tap. But for the UIStepper
, we need to take a little detour.
Custom Taps
The UIStepper
control has two halves, as shown below, so at this point it’s unclear what will happen if you just call tapViewWithAccessibilityLabel:
.
KIF will start out by trying to tap the center of the control. If it’s not a tappable area, it then tries points at the top-left, top-right, bottom-left, and then bottom-right. It turns out that tapping that center border between the plus and minus triggers the plus, so it will increment the stepper.
But what if you wanted to decrement the stepper? There are some workarounds, such as digging into the subviews to find the minus button. The other alternative is to use KIF’s tapScreenAtPoint:
test action, which will simulate a tap at any arbitrary point on the screen.
How’s your CGGeometry
knowledge? Can you figure out how to calculate the window coordinates of the plus and minus buttons? Ready to prove you’re awesome? Why not try this challenge? It is totally optional, and you can skip ahead to the code with the calculations below. But if you want to test your skills, try to code the calculations before peeking at the answer. By the way, the answer and full explanation are in the solution box.
[Note that you're going to add this code to the test shortly. This task is just to test your math and CGGeometry skills!]
Tapping the screen at an arbitrary point is sort of a last resort, but sometimes it is the only way to test your UI. For example, you might have your own custom controls that don’t implement the UIAccessibility
Protocol.
Finalizing the Timer
OK, so now you’re at the last steps of this Timer test. Are you realizing the awesome potential of UI testing with KIF? Good!
The final steps are to set the number of repetitions, and then start the timer. Add the following code to the end of test20StartTimerAndWaitForFinish
in UITests.m:
// 1 UIStepper *repsStepper = (UIStepper*)[tester waitForViewWithAccessibilityLabel:@"Reps Stepper"]; CGPoint stepperCenter = [repsStepper.window convertPoint:repsStepper.center fromView:repsStepper.superview]; CGPoint minusButton = stepperCenter; minusButton.x -= CGRectGetWidth(repsStepper.frame) / 4; CGPoint plusButton = stepperCenter; plusButton.x += CGRectGetWidth(repsStepper.frame) / 4; // 2 [tester waitForTimeInterval:1]; // 3 for (int i = 0; i < 20; i++) { [tester tapScreenAtPoint:minusButton]; } [tester waitForTimeInterval:1]; [tester tapScreenAtPoint:plusButton]; [tester waitForTimeInterval:1]; [tester tapScreenAtPoint:plusButton]; [tester waitForTimeInterval:1]; // 4 [KIFUITestActor setDefaultTimeout:60]; // 5 [tester tapViewWithAccessibilityLabel:@"Start Working"]; // the timer is ticking away in the modal view... [tester waitForViewWithAccessibilityLabel:@"Start Working"]; // 6 [KIFUITestActor setDefaultTimeout:10]; |
Here’s what’s going to happen in this final phase:
- The above code finds coordinates for the plus and minus buttons inside the
UIStepper
. The explanation for this code is contained in the spoiler box above. - The calls to
waitForTimeInterval:
add a delay so you can see the stepper value change – otherwise it goes by too fast for human eyes. - The stepper has a maximum value of 20, so tap the minus button 20 times to bring the value back to 1. Then, tap the plus button 2 times (interleaved with more small delays) to bring the number of repetitions to 3, the desired value.
- The default timeout for a test step is 10 seconds. Even in accelerated debug mode it’s possible for the 20+ minutes of “work” to take longer than 10 seconds, so set the timeout to a more generous number, 60 seconds.
- Tap the “Start Working” button, which will bring up the modal view controller. When all the repetitions have passed, the modal view controller will be dismissed. That means you’ll be back at the Timer view controller, so waiting for the “Start Working” button to appear again is effectively waiting for the modal view controller to go away.
- Reset the timeout back to 10 seconds to clean up.
Once you save the file, you’ll see a little diamond-shaped icon next to the method declaration:
This is a button to run a single test. So rather than firing up the entire test suite, you can run this one method in the test environment. Neat! Click on the diamond to run the test; you should see the simulator start up and then see the test run. Sit back and watch as it enters the task name, the sliders move and the timer ticks away. Without lifting a finger, you’re testing the UI.
Success! Now you can add successfully setting up tests to manipulate a variety of UI controls to your list of accomplishments.
If you run a single test with the diamond button, it only runs the single method and doesn’t call beforeAll
at the beginning. If your test depends on beforeAll
, you’ll still need to run the full test suite.
Ending Early
The “Get to Work!” modal view controller has a Give Up button that lets the user cancel the timer cycle. You’ll still be able to measure the number of minutes worked, even if you pull the plug on the test early. This data still gets logged in the history, but marked with a flag to indicate the full cycle didn’t finish.
But you don’t have to take my word for it, you can test it for yourself. Just do something very similar to the previous test. Set the timer parameters, tap the Start Working button, and then tap the Give Up button.
Don’t tap “Give Up” right away – the timer needs to tick for a bit so the app can create the history. So you can either hover over the test and kill it manually, or, you can program it to stop at a time and place of your choosing. If you enjoy hovering, feel free to skip this challenge. However, if you like to program apps to do the dirty work for you, try it out! Do you know how would add a little delay in between “Start Working” and “Give Up”?
Add the following test method to UITests.m directly below your other tests:
- (void)test30StartTimerAndGiveUp { [tester tapViewWithAccessibilityLabel:@"Timer"]; [tester clearTextFromAndThenEnterText:@"Give Up" intoViewWithAccessibilityLabel:@"Task Name"]; [tester tapViewWithAccessibilityLabel:@"done"]; [self selectPresetAtIndex:2]; [tester tapViewWithAccessibilityLabel:@"Start Working"]; [tester waitForTimeInterval:3]; [tester tapViewWithAccessibilityLabel:@"Give Up"]; [[tester usingTimeout:1] waitForViewWithAccessibilityLabel:@"Start Working"]; } |
After making sure you’re on the right tab, set the task name and select a preset. Then, start the timer and wait 3 seconds before giving up.
The final line of the method waits for the modal view controller to go away and for the main interface to return. Remember, the default timeout is 10 seconds, but it really shouldn’t take that long – tapping the “Give Up” button should dismiss the modal view controller immediately.
In the previous test, you used the class method setDefaultTimeout:
to set the timeout value globally for all test actions. Here, you’re calling usingTimeout:
to set a custom timeout for just this single step.
Save the file and click on the test’s diamond icon to run only this test. When the timer starts, you’ll see it tick for three seconds before it gives up and returns to the main screen.
History and Swiping
The History tab has not received much attention yet, but its time in the limelight is now. If you’ve worked through the exercises, you should have at least one entry in the history. Build and run the app, and switch to the History tab.
The History table view implements the new iOS 7 delete gesture — the one where you swipe the row to the left, and then tap on the “Delete” button that appears. There’s your next test! This requires at least one item in the History, so you need to be careful about running this one test individually. You need to ensure there is something in the history, otherwise there would be nothing to test! To be safe, you’ll run the entire test suite, since the earlier tests will create some history items for you to play around with. Remember the tests will run in alphabetical order. So you can be assured that the previous tests will create something to test the history screen with.
If you take a peek at tableView:cellForRowAtIndexPath:
in HistoryViewController.m, you’ll see that each cell gets an accessibility label such as “Section 0 Row 3″. This just helps KIF find the row. It is a poor label for real-world accessibility, so don’t use this in a real app. In this sample project, this has been #ifdef
‘d to debug builds only. You should do similar if you use this technique in your own apps. Be sure in release builds to set the accessibility label to something useful for users of accessibility features.
Now, open UITests.m and add the following test method:
- (void)test40SwipeToDeleteHistoryItem { // 1 [tester tapViewWithAccessibilityLabel:@"History"]; // 2 UITableView *tableView = (UITableView *)[tester waitForViewWithAccessibilityLabel:@"History List"]; NSInteger originalHistoryCount = [tableView numberOfRowsInSection:0]; STAssertTrue(originalHistoryCount > 0, @"There should be at least 1 history item!"); // 3 [tester swipeViewWithAccessibilityLabel:@"Section 0 Row 0" inDirection:KIFSwipeDirectionLeft]; [tester tapViewWithAccessibilityLabel:@"Delete"]; // 4 [tester waitForTimeInterval:1]; NSInteger currentHistoryCount = [tableView numberOfRowsInSection:0]; STAssertTrue(currentHistoryCount == originalHistoryCount - 1, @"The history item was not deleted :["); } |
Here’s what’s going on in the above method:
- Switch to the History tab.
- Get a reference to the table view and keep track of how many rows are present. The test will fail if there isn’t at least one row.
- Swipe the table view cell to the left. When the Delete button appears, tap it.
- The table view performs an animation when it deletes a cell, so add a short delay before continuing. Check the number of rows in the table view again; there should be one less row than before.
Run the entire test suite using Command-U to see everything run in sequence.
Your tests now cover the basic flow of the app – from resetting data to running the timer a few times, to verifying and deleting history.
If you were to continue developing the app, these tests would be a good baseline to ensure there are no regressions in the interface. That means you also need to update the tests every time the interface changes – tests are like specifications for your app and they need to stay up-to-date to be useful.
Where To Go From Here?
By now, you should have a good sense of the possibilities of KIF and your mind should be racing with ideas about how to utilize this highly functional testing for your own apps. To get started with adding KIF to your project, check out the documentation. You can add KIF manually or use the very convenient CocoaPods dependency manager.
What’s cool about KIF is that it is an open-source project and new features are constantly in development. For example, at the time of writing this tutorial, the next release will likely include a feature to take a screenshot of your app programmatically and save it to disk. This means you should be able to run the tests and review key points in the process by reviewing screenshots at your leisure. Doesn’t that sound a million times better than hovering and watching KIF tap and swipe its way through your app? KIF just keeps getting better and better, so learning how to use it is a wise investment in yourself.
Finally, since KIF test cases are subclasses of OCUnit and run within the standard Xcode 5 test framework, you can run the tests through your continuous integration process. Then you can truly have bots with quasi-touch fingers testing your app while you do other things! Woo-hoo!
You can download the final project archive here. If you have any questions or comments, please join the forum discussion below. Happy testing!
iOS UI Testing with KIF is a post from: Ray Wenderlich
The post iOS UI Testing with KIF appeared first on Ray Wenderlich.