Test Driven Development for iPhone

UPDATE:
Check out the project source code at: http://github.com/blazingcloud/iphone_logic_testing

Here at Blazing Cloud we really like Test-Driven Development (TDD). We try to use it for all of our development projects, and we recently learned how to do it for IPhone development.

The unit test tool available for Cocoa development is more like JUnit for Java than RSpec for Ruby. Each test is implemented as a method, and it is described by its method name, so you need to get creative with camel case. In addition, the error messages that you get out of the c compiler are difficult to parse (at least to those of us with limited c experience), which makes the watch it fail first and then fix it methodology difficult. However, it is still worth suffering in the unfriendly environment to get the benefits of a full test suite. Hopefully the knowledge that we gained in muddling through this will help you get started in your own project.

There are two types of tests suites for IPhone development: Logic Tests and Application Tests.

  • Logic Tests test business logic and are run independently of any device (including the emulator).
  • Application Tests test the UI and everything that can only be tested on the device.

This article will detail how to develop business logic code using Logic Tests. We will follow up with Application Testing in a subsequent article. The Apple developer site has documentation for Unit Testing Applications, and you may want to read this document first. We will walk through the set-up in this article but if you need more detail you may find it in the Apple site.

Logic Testing

We will now demonstrate Logic Testing using a simple tip calculator as an example. To begin (with iPhone SDK and Xcode installed) launch XCode and create a new View Based IPhone Application called TipCalc. As a sanity check you can try to run this empty app in the simulator and you will see an empty view screen.

Make a new target for your logic tests. Right click Targets and select Add -> New Target. Select Unit Test Bundle and name the bundle “Logic Tests”. You can close the little info window that comes up, and you will see your new target in the Targets group.

Create a new group under the TipCalc main group called “LogicTests”. This will look like a directory under your TipCalc application as shown here:

Add a new file to the LogicTests group that is of type Objective-C test case class and call it “TipCalcTests.m”. Make sure the box is checked to create the associated header (.h) file, and select only the Logic Tests target to add the new file to. When you hit Finish the two new files will be added.

At this point you should try to run your tests. Make Logic Tests your active target and click Build and Run. You won’t really see anything happen when you do this, but look in the lower right corner of your XCode window:

Click on the red error icon to open up the build results window. The errors you see won’t really make any sense at this point, and I think its because they are generated during the run step and this target doesn’t have an executable to run. If you click Build instead of Build and Run you will get a more informative error. You don’t actually have to run anything because the tests are executed during the build step.

The new error should look like this:

This error is happening because when you create a new test case file by default it is set up to be an Application test, not a Logic test, so it expects to find an ApplicationDelegate instance, which is only present in the running app. You can remove the code that is for application testing from these test files, as you will make separate test files for those tests. In your header file remove these lines:

[objc]
// Application unit tests contain unit test code that must be injected into an application to run correctly.
// Define USE_APPLICATION_UNIT_TEST to 0 if the unit test code is designed to be linked into an independent test executable.

#define USE_APPLICATION_UNIT_TEST 1
[/objc]

and these lines:

[objc]
#if USE_APPLICATION_UNIT_TEST
- (void) testAppDelegate; // simple test on application
#else
- (void) testMath; // simple standalone test
#endif
[/objc]

so that you just have an empty header file. From the implementation file you can remove these lines:

[objc]
#if USE_APPLICATION_UNIT_TEST // all code under test is in the iPhone Application

- (void) testAppDelegate {

id yourApplicationDelegate = [[UIApplication sharedApplication] delegate];
STAssertNotNil(yourApplicationDelegate, @"UIApplication failed to find the AppDelegate");

}

#else // all code under test must be linked into the Unit Test bundle

- (void) testMath {

STAssertTrue((1+1)==2, @"Compiler isn’t feeling well today :-(" );

}
#endif
[/objc]

so you have an empty implementation file. Now if you hit Build again you will not have any errors, and we can start writing some tests.

In the spirit of TDD we will write a test first before we write any other code. Add the following code to your implementation file:

[objc]
- (void) testTipCalculation {

float percentage = .20;
float bill = 34.45;
float expectedTotal = percentage * bill;
NSLog(@"expected total: %f", expectedTotal);
float result = [TipCalculator calculateTipFor:bill andPercentage:percentage];

STAssertEquals(expectedTotal, result, @"Tip not calculated correctly. Expected %f but got %f", expectedTotal, result);
}
[/objc]

and the corresponding declaration in your header file:

[objc]
@interface TipCalcTests : SenTestCase {

}

- (void) testTipCalculation;

@end
[/objc]

Build again and you will get a useful error: “‘TipCalculator’ undeclared (first use in this function)” because we haven’t defined the TipCalculator class. Add the TipCalculator class to the Classes group and add it to both the TipCalc and Logic Tests targets. Edit TipCalcTests.h and import TipCalculator.h like so:

[objc]
#import "TipCalculator.h"
[/objc]

Run your tests again and you will see the following results:

As you can see, the error message isn’t particularly helpful, but there is a warning that tells us “‘TipCalculator’ may not respond to ‘+calculateTipFor:andPercentage:’”

Add the method declaration now to your TipCalculator header:

[objc]
+ (float)calculateTipFor:(float)bill andPercentage:(float)percentage;
[/objc]

and a stub for the method in your implementation file that returns a hard coded value:

[objc]
+ (float)calculateTipFor:(float)bill andPercentage:(float)percentage {
return 0.8;
}
[/objc]

Now when you build you will get a useful error telling you that your test failed:

Notice that the only thing that it prints out is the error message you specifically told it to generate including the expected and actual values. So you will need to make sure your test results statements are meaningful.

If you run into issues it might be useful to log things to the console. In the sample code there is an NSLog statement:
[objc]
NSLog(@"expected total: %f", expectedTotal);
[/objc]

This output doesn’t get written to the run console in XCode (maybe because the tests are executed during the build step, not the run step?) but it does get written to the system console. You can find the system console by typing “console” into spotlight. The system console looks like this:

A lot of messages get printed here, but if you filter the list by the string “otest” you will see your messages. If anyone knows how to get log output to show up in XCode please leave us a comment.

If you do want to launch your app in the simulator every time you run your tests you can just drag you TipCalc target into your Logic Tests target and click Build and Run instead of Build.

So that’s basically it, now you know how to write Logic Tests. You can find a complete list of test macro functions (like STAssertEquals) in the Unit-Test Result Macro Reference in the Apple site.

6 Comments

  1. Posted March 16, 2010 at 4:46 pm | Permalink

    Neat- I’ve been doing iPhone stuff lately, so this is super timely. It’s good to see the TDD stuff in a new enviroment, too.

  2. Posted March 21, 2010 at 3:26 am | Permalink

    Thanks! I was wondering why the automatically generated test classes failed the first build. Good to know!

  3. Posted April 6, 2010 at 7:41 am | Permalink

    Check out UISpec (http://www.iphonetesting.com)…it’s an open-source BDD framework like RSpec that supports logic and application level tests. We use it to test our iphone application TripCase.

  4. Amit
    Posted May 3, 2010 at 10:25 pm | Permalink

    Thanks, nice tutorial.

  5. Posted May 20, 2010 at 2:33 pm | Permalink

    I enjoyed reading your blog. I just read that Brits vote iPhone 8th greatest invention. I love my Iphone.

  6. James Lubowich
    Posted August 17, 2010 at 9:07 pm | Permalink

    Thanks so much for this post!!

    I have been looking everywhere to find out how to do this.

    The article at apple was not updated and is lacking some details.

2 Trackbacks

  1. By Build-in Quality « Scrum Bytes on June 8, 2011 at 1:02 am

    [...] Is the job of testing or QA to find defects? Traditionally, yes. Would we rather prevent defects from being created or find them after they exists? Clearly prevention is less expensive and faster than doing something wrong and then fixing it. The keys to this are Test Driven Development, Continuous Integration and Automated Acceptance Testing. If you haven’t yet, check out some of these articles: iOS Development http://developer.apple.com/tools/unittest.html http://geekdamana.blogspot.com/2009/01/tdd-objective-c-testing-cocoa-framework.html http://code.google.com/p/google-toolbox-for-mac/ http://blazingcloud.net/2010/02/20/test-driven-development-for-iphone/ [...]

  2. [...] Test Driven Development for iPhone – Here at Blazing Cloud we really like Test-Driven Development (TDD). We try to use it for all of our development projects, and we recently learned how to do it for IPhone development. (tags: tdd bdd ios objectivec development code) [...]

Post a Comment

Your email is never shared. Required fields are marked *

*
*