Pretty image
Almost a year ago in these pages, Ian Dees showed how to use Cucumber to test your iPhone apps. Now iCuke makes it even easier.

Aslak Hellesoy’s Cucumber has seen huge success helping developers and project owners clearly communicate desired behavior in the Ruby on Rails arena.

Using the same concepts that enable Cucumber to test Web applications, Unboxed Consulting’s iCuke now makes it easy to test iPhone applications by providing a Web interface to the iPhone simulator and some predefined steps for common user actions such as typing text, tapping, or swiping.

Despite the growth of the iPhone development platform, there still aren’t many aids for integration testing, and what aids there are mostly require compiling extra code into your application and writing tests in Objective-C. Apple UI Automation instrument in SDK 4.0 is a start, but even it requires manual operation—and it’s no use in CI environments.

iCuke is different from previous solutions in that it doesn’t require compiling extra code into your application or adjusting your Xcode project to add new targets. Once you install the iCuke gem, you can start writing tests straight away, without any changes to your application.

If you’ve used Capybara or Webrat to test Web applications with Cucumber, you should be right at home using iCuke.

For this article I’ll assume that you’re familiar with Cucumber. But if you haven’t used it before, you can find a wealth of information at the Cucumber website.

Getting started

To get started testing your iPhone application with Cucumber, just install the iCuke gem and then run icuke . (note the period: that’s “icuke .”) in the iPhone application’s directory. This will set up Cucumber for the project with an example feature file to give you an idea of what is possible.

Let’s give it a go using one of Apple’s sample iPhone applications called QuickContacts. You can find QuickContacts by searching for it in the Xcode help system. Click the “Open Project in Xcode” button and choose somewhere to save the project.

Once you have the project open, hit Build and Run, and Xcode should compile the application and then launch it in the simulator.

Now that we have an application, let’s get iCuke installed and start writing some tests for it.

Find your way to the application directory in Terminal and run icuke .. It will add a features directory with a feature file in it (along with the support and step_definitions directories, which we don’t need to touch just yet).

The example story won’t make much sense in relation to the application we’re working on, as it’s just a generic example. As the application is to manage contacts, a good first story might be to test adding a contact.

Have a quick look at the features/example.feature file to get a feel for the kind of stories you can write, but then you can delete the file.

Open up a new file features/add_a_contact.feature in your editor and paste this feature in:

 Feature: Adding a contact
  In order to remember my friend’s phone number
  As a forgetful user
  I want to add my friend to my contact list
 
  Background:
  Given "QuickContacts.xcodeproj" is loaded in the simulator
 
  Scenario: Adding a contact
  When I tap "Create New Contact"
  And I type "Joe" in "First"
  And I type "Bloggs" in "Last"
  And I tap "Done"
  And I tap "Display Picker"
  Then I should see "Joe Bloggs"

Save the file and then try running cucumber from the application directory. You should see the application loading in the simulator and performing the steps in the story. When it’s done, cucumber should happily report that everything went as expected. Our first story passes!

You may notice, however, that if you run the story again, there are now two Joe Bloggs in the address book. This is because the iPhone simulator doesn’t wipe the address book between runs. In order to fix that, we’ll need to use an iCuke module, which we’ll discuss later on.

Getting more involved

Now that we have our first story passing, let’s make sure we can look up a contact again.

Open up a new file features/searching_for_a_contact.feature in your editor and paste this feature in:

 Feature: Searching for a contact
  In order to find my friend’s phone number
  As a forgetful user
  I want to find my friend in my contact list
 
  Background:
  Given "QuickContacts.xcodeproj" is loaded in the simulator
 
  Scenario: Finding a contact
  When I tap "Create New Contact"
  And I type "Joe" in "First"
  And I type "Bloggs" in "Last"
  And I tap "Done"
  And I tap "Display Picker"
  And I type "Joe" in "Search"
  Then I should see "Joe Bloggs"

Save the file and run cucumber again. That story should pass as well, so we can find a contact that we’ve added. But what if we had 20 people in the contact list? We could change the story so that we go through the motions of adding 20 contacts, but that would make the story quite long. We could wrap the steps up into a new step so we’re able to say “Given there are 20 contacts”. That would clean the story up, but it would take a long while to do all the tapping and typing to enter those contacts. If we want to speed things up we can take advantage of the fact that iCuke supports loading application-specific modules written in Objective-C.

As an aside, you will notice that iCuke seems to pause quite a lot between actions. This is because it’s very difficult for iCuke to know when the application has finished transitions between views or updated the screen in reaction to an event. Unfortunately, this means being very conservative speed-wise to ensure that we don’t try to begin an action before the application is ready. Hopefully future versions of iCuke will be able to reduce the pauses. It seems like the new UI Automation code from Apple may be helpful in this regard.

Adding a contact to the iPhone address book

In order to add a contact without using the iPhone UI we need to use something like the following Objective-C:

 #import <AddressBook/AddressBook.h>
 
 ABRecordRef record = ABPersonCreate();
 ABAddressBookRef addressBook = ABAddressBookCreate();
 
 ABRecordSetValue(record, kABPersonFirstNameProperty,
  CFSTR("Test"), NULL);
 ABRecordSetValue(record, kABPersonLastNameProperty,
  CFSTR("User"), NULL);
 
 ABAddressBookAddRecord(addressBook, record, NULL);
 
 ABAddressBookSave(addressBook, NULL);

The problem is getting iCuke to call some custom Objective-C code. This is where module loading comes in.

iCuke talks to Cucumber via an HTTP interface that is loaded into the iPhone Simulator alongside your application.

iCuke’s module loading system enables you to add code to the HTTP server so that it can perform actions specific to your application in response to HTTP requests. In our case we want to add a module to create a new contact when we GET “/add_contact”. We can pass data to the module via the HTTP query using JSON. To add a contact “John Smith” we could use “/add_contact?{‘first_name’: ‘John’, ‘last_name’: ‘Smith’}”.

Note: This isn’t really how HTTP is supposed to work. As we are changing the state of the application, this should really be a POST action. A future iCuke release may fix this.

In order to wrap the required Objective-C code into an iCuke module we need to add some HTTP interface structure around it. As well as adding a contact, let’s add the ability to wipe the address book. This is what the finished module looks like:

 #import <AddressBook/AddressBook.h>
 #import "iCukeHTTPServer.h"
 #import "iCukeHTTPResponseHandler.h"
 #import "JSON.h"
 
 @interface ContactResponse : iCukeHTTPResponseHandler {
 }
 @end
 
 @implementation ContactResponse
 + (void)load
 {
  [iCukeHTTPResponseHandler registerHandler:self];
 }
 
 + (BOOL)canHandleRequest:(CFHTTPMessageRef)aRequest
  method:(NSString *)requestMethod
  url:(NSURL *)requestURL
  headerFields:(NSDictionary *)requestHeaderFields
 {
  if ([requestURL.path isEqualToString:@"/add_contact"] ||
  [requestURL.path isEqualToString:@"/wipe_contacts"]
  ) {
  return YES;
  } else {
  return NO;
  }
 }
 
 - (void)startResponse
 {
  NSString *json = [[url query]
  stringByReplacingPercentEscapesUsingEncoding:
  NSUTF8StringEncoding];
  id contact_data = [json JSONValue];
 
  ABRecordRef record = ABPersonCreate();
  ABAddressBookRef addressBook = ABAddressBookCreate();
 
  if ([url.path isEqualToString:@"/wipe_contacts"]) {
  CFArrayRef people = ABAddressBookCopyArrayOfAllPeople(addressBook);
  for (int i = 0; i < CFArrayGetCount(people); i++) {
  ABAddressBookRemoveRecord(addressBook,
  CFArrayGetValueAtIndex(people, i), NULL);
  }
  }
  else if ([url.path isEqualToString:@"/add_contact"]) {
  ABRecordSetValue(record, kABPersonFirstNameProperty,
  [contact_data objectForKey: @"first_name"], NULL);
  ABRecordSetValue(record, kABPersonLastNameProperty,
  [contact_data objectForKey: @"last_name"], NULL);
 
  ABAddressBookAddRecord(addressBook, record, NULL);
  }
 
  ABAddressBookSave(addressBook, NULL);
  CFHTTPMessageRef response =
  CFHTTPMessageCreateResponse(kCFAllocatorDefault,
  200, NULL, kCFHTTPVersion1_1);
  CFHTTPMessageSetHeaderFieldValue(response, (CFStringRef)@"Connection",
  (CFStringRef)@"close");
 
  CFDataRef headerData = CFHTTPMessageCopySerializedMessage(response);
 
  @try
  {
  [fileHandle writeData:(NSData *)headerData];
  }
  @catch (NSException *exception)
  {
  // Ignore the exception, it normally just means the client
  // closed the connection from the other end.
  }
  @finally
  {
  CFRelease(headerData);
  [server closeHandler:self];
  }
 }
 @end

Save that to a file features/support/modules/contact.m

Then we need to compile the module. iCuke provides a helper script for this so you can run icuke-module 4 contact.m from the features/support/modules directory and iCuke should compile the module into a dynamic library contact-sdk4.dylib. The 4 argument to icuke-module tells it which SDK to compile against. If you are working on a 3.1 application you’d need to use icuke-module 3 contact.m.

To add steps that create address book entries using the new module, create the directory features/step_definitions and save a new file features/step_definitions/contact_steps.rb:

 Given /^the address book is empty$/ do
  simulator.get ’/wipe_contacts’
 end
 
 Given /^a contact named "[^\"]*"$/ do |name|
  first_name, last_name = name.split(’ ’)
  simulator.get ’/contact’, :query => { :first_name => first_name,
  :last_name => last_name }.to_json
 end
 
 Given /there are (\d+) contacts named "[^\"]*"$/ do |count, name|
  count.to_i.times { Given %Q{a contact named "#{name}"} }
 end

Now we can rewrite our contact search story (the line broken here for publishing purposes should be entered as one line):

 Background:
  Given "QuickContacts.xcodeproj" is loaded in the simulator
  And the module "features/support/modules/contact" ¬
  is loaded in the simulator
  And the address book is empty
 
 Scenario: Finding a contact
  Given there are 20 contacts named "Anna Bloggs"
  And a contact named "Joe Bloggs"
  When I tap "Display Picker"
  And I type "Joe" in "Search"
  Then I should see "Joe Bloggs"

There, that’s concise and quick to run. You can use this technique to set up any fixture data your app requires in order to test scenarios quickly without having to crawl through the same UI repeatedly.

Troubleshooting

Sometimes stories won’t work as you expect, and it might not be clear why. It’s because iCuke uses Apple’s accessibility API to detect what is on the iPhone’s screen.

For an example of this, try adding: But I should not see “Anna Bloggs” to the end of the “Finding a contact” scenario. You’ll notice that it fails, even though it should pass, considering what is really on the screen.

Unfortunately this is one of the times that the accessibility API misleads iCuke. The standard search result interface overlays the search results over the unfiltered table, so they still appear in the view hierarchy (as you can see from the XML in iCuke’s error message). There is no way, as of this writing, for iCuke to tell if the cells below the search results are really visible, so “should see” and “should not see” can’t be 100 percent accurate.

Hopefully this kind of issue will be handled by iCuke better in a future release. Please feel free to file issues on github for any such problems!

Rob’s main focus is writing and improving development tools, streamlining the development workflow and making it easier to test code. He strives to be pragmatic, although perfectionism often creeps in. He enjoys the challenge of learning new open source code and working with the authors to adapt it or improve it, via his consultancy, The IT Refinery.

Send the author your feedback or discuss the article in the magazine forum.