2009
4/2

iPhone Unit Testing with OCUnit







6Tringle's featured product: PhotoClay Logo Photo mushing fun with your finger

Buy app through iTunes








We enthusiastically recommend:

There are a scattering of iPhone based testing frameworks and utilities:

Google's was one of the first. They've apparently built a lot of infrastructure around their solution to fit within existing test systems. With a recent Xcode update, Apple's brought back the original Objective-C test solution.

OCUnit

If you're coming from GTM, then the biggest difference is that OCUnit is the integration into Xcode. This means you get IDE errors instead of having to parse the console output (plus!). It also means you have to go to the console to read your log messages, rather than having them display in the built-in console (minus!).

This one ships with Apple, but the only clear instructions come from Sente (the original author of OCUnit, back in 1998).

Lecture 19 of the Stanford iPhone class had some slides and an example project. Unfortunately, due to the nature of the Stanford website, it's unlikely the slides and sample files will reappear on the website until Lecture 19 is presented for the current semester. Even when the links existed however, the Stanford way didn't give detailed instructions. In the mean time, all I can say is that you trust me when I say that the Stanford way of linking is much easier to do.

The Sente method of having the test run every time you build is far superior to the Stanford method (you have to manually change the target and run the tests). If you aren't continually running your tests, they approach a moldy and lifeless state.

Here's a set of instructions that take the best of both worlds

  1. Add a new Unit Test bundle target (Project -> New Target... -> Mac OS X -> Unit Test Bundle)

    [ScreenShot of New unit test bundle] (/media/images/NewTargetUnitTestBundle.png)

  2. Modify the "Other Linker Flags" setting.

    We need to change the Cocoa value to Foundation. This will allow us to compile for the iPhone Simlulator

    [ScreenShot of other linker flags] (/media/images/UnitTestBundleOtherLinkerFlags.png)

    [ScreenShot of other linker flags edit] (/media/images/UnitTestBundleOtherLinkerFlagsEdit.png)

    Lastly, search for any Cocoa.h references and delete them:

    [Cocoa.h reference in build settings] (/media/images/UnitTestBundleSearchCocoah.png)

    If you prematurely built and got the following Cocoa.h errors:

    /System /Library /Frameworks /Cocoa.framework /Headers /Cocoa.h:13:26: error: AppKit/AppKit.h: No such file or directory /System /Library /Frameworks /Cocoa.framework /Headers /Cocoa.h:14:30: error: CoreData/CoreData.h: No such file or directory

    You may need to do a Clean All Targets to clear them. Somewhat counterintuitively, if your project does not contain any tests, it will register a test failure.
    Another note about test failures:

    /Developer /Tools /RunPlatformUnitTests.include: 384: error: Failed tests for architecture 'i386' (GC OFF)

    The description here about failure has nothing to do with the architecture or garbage collection. It's just information for your reference about the environment in which the test failed. The part you need to pay attention to is Failed tests. Similarly, there is nothing wrong with the RunPlatformUnitTests.include script.

  3. Add a test file with a TestCase subclass in it.

    Using the Mac OS X -> Cocoa -> Unit Test Case Class Template, create and add a new test case file.

    Test Case files MUST end in TestCase (ie. MathTestCase or BackEndTestCase). Similarly, all test methods should start with test. The underlying tech mechanism uses some very nifty runtime magic to determine which classes and methods to run by their names.

    [] (/media/images/XcodeAddUnitTestCase.png)

    Make sure you are only adding to the Unit Test Bundle and not to the main Project

    [] (/media/images/XcodeAddUnitTestCaseAddToProject.png)

  4. Add a test

    Here's a easy one you can use to test the test framework itself. (How very meta.)

    - (void)testTestFramework
    {
        NSString *string1 = @"test";
        NSString *string2 = @"test";
        STAssertEquals(string1, 
                       string2, 
                       @"FAILURE");
        NSUInteger uint_1 = 4;
        NSUInteger uint_2 = 4;
        STAssertEquals(uint_1, 
                       uint_2, 
                       @"FAILURE");
    }
    
  5. Build the test bundle, make sure tests pass

  6. Build the test bundle, make sure tests fail

    Again, just to exercise your test framework, temporarily change one of the strings to force a test failure.

    [Force a failure] (/media/images/UnitTestForceAFailure.png)

  7. Add the unit test bundle as a dependent

    Now, we want the unit tests to run every time we build. We can do this by setting a dependency on the Unit Test Bundle. Switch back to the main project target (away from the unit test bundle) and Edit the Active Target. Click the + button under to add and new Direct Dependancy

    [Add UnitTestBundle dependancy] (/media/images/XcodeAddUnitTestBundleDependancy.png)

  8. Build the app, make sure test pass and fail

Now you have an automated, up to date testing framework in place. Every time you build, you will be immediately notified of any test regressions. The rest is attitude. Xcode will keep the tests running every build, but you have to keep writing tests when appropriate.

Sample project available at 6Tringle's github repository.

An aesthetic note about header files

Test Case files almost always have an nearly useless header file, so I usually get rid of them, creating single file test cases. The resultant .m looks like this:

//only run on the simulator
#include "TargetConditionals.h"
#if !TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
#import <SenTestingKit/SenTestingKit.h>
@interface SampleTestCase : SenTestCase 
{
}
@end
@implementation SampleTestCase
- (void) setUp
{
    // Optional
}
- (void) tearDown
{
    // Optional
}
- (void)testTestFramework
{
    NSString *string1 = @"test";
    NSString *string2 = @"test";
    STAssertEqualObjects(string1, 
                         string2, 
                         @"FAILURE");

    NSUInteger uint_1 = 4;
    NSUInteger uint_2 = 4;
    STAssertEquals(uint_1, 
                   uint_2, 
                   @"FAILURE");
}
@end
#endif

Products of unassailable virtue and rectitude

about products blog contact misc xml