Unit Testing with OCUnit

Fully testing an iPhone App is a big topic but a good place to start is to add unit tests for the Model classes. Testing the model avoids some of the complexity of testing UI elements but still has considerable benefit. XCode is already integrated with OCUnit (aka SenTestCase) so getting setup for unit testing is fairly painless.

Unit Testing

Of course “everybody” knows that it is a good idea to test your code but in my experience adding tests to a project that is approaching completion is a painful activity. If you get into the habit of writing tests as you write the code you not only reduce the pain but you may also find it has a positive impact on the quality of the code. There is something about the discipline of thinking about how you can easily test a piece of code that encourages you to keep it simple with well defined interfaces, minimal coupling and dependencies.

I don’t intend this to be a post about Test Driven Development (TDD). I see lots of benefits to the TDD approach but writing your tests before you write your code is but one approach. If I am being honest I tend to iterate between writing some code and/or writing the tests for that piece of code first. Either way can work but I guess the important thing is to do it and do it early.

Most iPhone applications follow the Model View Controller (MVC) pattern that is also common to Mac Cocoa and Ruby on Rails applications. I find an easy way to get started if you are not yet convinced about testing an iPhone App is to focus first on the model. Testing the model is a lot easier than testing User Interface elements and can also be done in the Simulator. The steps to get your Xcode project setup for unit testing are really easy so I would encourage you to always add a Unit Testing target when you first create a project.

I should also clarify that when I talk about Unit Tests I tend to mean tests that can be run in isolation of the full application. Apple refer to these as Logic Tests in their documentation. In an MVC application this most often means testing the Model as the Controller and View tests normally require a fully running application. I refer to the full application tests as Integration Tests for obvious reasons and will try to cover those in a future post.

The Sample Project

So having said that you should build your tests as you develop your application I will now completely ignore that advice by adding some unit tests to an existing project. The main point of this post is really to show you how easy Xcode makes it to get started with unit testing rather than to demonstrate TDD. I will start with the sample Apple project called TheElements as it provides a well structured Model-View-Controller application and look at how we can apply some unit tests to its model classes.

Setting up the project for Unit Testing

The first step in adding unit tests to the Xcode project is to create a new target that is used to build and run the unit tests. Right-click on the Targets in the Groups & Files section of the project window and select Add -> New Target… to bring up the template dialog window and select the Unit Test Bundle.

Name the new target UnitTests and add a group with the same name to the Groups & Files list which we will use to hold our unit test code. The basic idea is now to add a unit test class for each model class in our project. Each unit test we add will be responsible for unit testing its corresponding model class.

Adding a Unit Test Class

Our example project contains a model class named AtomicElement so with UnitTests set as the active target add a new unit test class named AtomicElementTests that will contain our test cases for this class. Xcode contains a ready made template for Objective-C test classes that we can use for this:

It is important to ensure that this file is only added for the UnitTests target and not for the main application target (named TheElements in this case).

Also since we want to test the AtomicElement class we need to add it to our UnitTests target. Right-click on the class file AtomicElement.m and select Get Info and then on the Targets tab ensure that the UnitTests target is selected in addition to the normal application target:

Note: If you are following along there is a modification that you need to make to AtomicElements.h to avoid some build errors. You need to import the standard UIKit header file. This is normally imported by the pre-compiled header file TheElements_Prefix.pch when building the standard app. Since our UnitTests target does not use this pre-compiled header file we will import it directly by adding the following line to AtomicElement.h

#import <UIKit/UIKit.h>

If all has gone well your Xcode project should now have the following additions for the UnitTest target:

A First Test Case

The default template unit test classes include some conditional code to allow you to write both unit and integrations tests. Since we are only interested in writing unit tests for our models we can simplify the code. The header file for AtomicElementTests.h looks like the following:

//  AtomicElementTests.h
#import <SenTestingKit/SenTestingKit.h>
#import <UIKit/UIKit.h>

@interface AtomicElementTests : SenTestCase {
  NSArray *testData;
}

@property (nonatomic, retain) NSArray *testData;

@end

I will explain the testData member variable in a moment but first it is worth understanding the basic structure of a test. As I already mentioned the basic strategy is to add a test class for each of our model classes that will contain the test cases for that model. Each of these test classes inherits from the SenTestCase base class.

To add a unit test case we simply add an instance method to this class. The only constraint is that the instance method name should start with “test” and have a return type of void. When we build and run our UnitTests target all of these test methods are run automatically for us.

We will make our first test case very simple to demonstrate the basic idea. As a first test case we will validate that we can successfully allocate and initialise an AtomicElement object. Our model contains an initializer named initWithDictionary that we will test. However this method expects a dictionary containing values that are used to initialise the object.

The ability to generate test data is one of the challenges of writing unit tests. The SenTest framework that Xcode uses makes it easy to manage this test data once for all the test cases in the class through the setUp and tearDown methods. So to allocate our test data we add the following method to our class:

- (void)setUp {
    // read in the test data
    NSBundle *thisBundle = [NSBundle bundleForClass:[self class]];
    NSString *testFile = [thisBundle  pathForResource:@"Elements"
                                               ofType:@"plist"];
    self.testData = [[NSArray alloc] initWithContentsOfFile:testFile];
}

This relies on the fact that the project already has a plist file (which we need to add to the UnitTests target) containing data for the elements that we can reuse as test data. Note that the plist file would normally be loaded from the mainBundle for iPhone OS applications but for our UnitTest target this assumption does not apply so we get the bundle name from the class.

To release the test data once the unit tests have completed we also add a tearDown method as follows:

- (void) tearDown {
  [testData release];
}

The setUp method will always be called before our test methods run and the tearDown method will always be called after all of our test methods have finished. Now we can add our first real unit test that will simply create a new object and test that it worked:

- (void) testCreationAtomicElement {
  NSDictionary *testItem = nil;
  
  // iterate over the values in the raw elements dictionary
  for (testItem in testData)
  {
    // create an atomic element instance for each
    AtomicElement *element = [[AtomicElement alloc] initWithDictionary:testItem];

    // Did we get back a valid element?
    STAssertNotNil(element, @"Unable to allocate Atomic Element");
  
    // Check the atomic number
    NSInteger expectedNumber = [element.atomicNumber integerValue];
    NSInteger actualNumber = [[testItem valueForKey:@"atomicNumber"] integerValue];
    STAssertEquals(expectedNumber, actualNumber,
                       @"atomicNumber should be %d but got %d",
                       expectedNumber, actualNumber);

    // Test the other member variables...
    // ...
    
    [element release];
  }
}

To run the test we just need to build the UnitTests target. If the tests are successful the build will complete with no issues. Some comments about this test case:

  • the test cycles though our test data, allocating a new object each time and then validating that the new object contains the values that we expect from out test data.
  • the STAssertNotNil macro is used to test that an object was allocated.
  • the STAssertEquals macro is used to test that a member variable has the value that we expect
  • to keep the example brief we only test for a single member variable (atomicNumber)

As I was looking at the AtomicElement class whilst writing the above test it became obvious that there is really no validation in the initializer method. Of course, this is only example code from Apple but what happens if we attempt to create a new object but do not pass a valid dictionary to the initializer?

Making the Test Pass

We are starting to adopt some of the workflow from Test Driven Development. We will add a test where we attempt to create a new AtomicElement object with an empty dictionary:

- (void)testEmptyDictionary {

  AtomicElement *element = [[AtomicElement alloc] initWithDictionary:nil];
  STAssertNil(element, @"AtomicElement creation should fail");
}

Now when we build our UnitTests we get a failure as our model does no checking and blindly allocates an empty object:

We have defined how our code should behave in the unit test and since our model does not currently check if it is passed a valid dictionary for initialisation our tests fails. To fix this we need to change our model to validate the dictionary parameter:

- (id)initWithDictionary:(NSDictionary *)aDictionary {

  if (aDictionary == nil) {
    [self release];
    return nil;
  }

Now when we build the UnitTests target our tests work and we have made a small but important improvement to the model. We can continue this stepwise refinement by testing other edge cases and error conditions but hopefully you already get the idea. The iPhone Development Guide contains a good list of the STAssert macros that you can use in your test cases.

Summing Up

This has turned into a long post but I hope if you have never tried unit testing your iPhone application that I have persuaded you to at least give it a try. By starting with your model classes the learning curve is fairly gentle. Testing the model may only test a fraction of your code but it gives you a firm foundation on which to build. Also the skills that you learn will be a big help when it comes to testing the controller and view code - a topic I hope to cover in a future post.