macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Unit Testing with OCUnit
Pages: 1, 2

Writing Tests

The easiest way to implement test cases using OCUnit is to implement them as subclasses of SenTestCase. Here is a complete (though by no means thorough) test case for the CheckbooX CheckbookEntry class.


// TestCheckbookEntry.h
#import <SenTestingKit/SenTestingKit.h>

@class CheckbookEntry;

@interface TestCheckbookEntry : SenTestCase
{
    CheckbookEntry *entry;
}

// Note that you don't have to declare your test
// methods here.

@end

// TestCheckbookEntry.m
#import "TestCheckbookEntry.h"
#import "CheckbookEntry.h"

@implementation TestCheckbookEntry

- (void) setUp {
    entry = [[CheckbookEntry alloc] init];
}

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

- (void) testEntryAmount {
    [entry setAmountInPennies:123];
    STAssertEquals(123L, [entry amountInPennies],
                   @"bad amount; 123 != %ld",
                   [entry amountInPennies]);
    STAssertEquals((float)1.23, [entry amount],
                   @"bad amount; 1.23 != %f",
                   [entry amount]);
}

@end

I added a new "Testing" group to the top level of the project. Next, I created a new Objective-C class named TestCheckbookEntry, remembering to uncheck the "CheckbooX" target and to check the "Testing" target instead.

I entered and saved the code above. Next, I tried running this simple test.

Running Tests

I made sure that "Testing" was the active target and clicked the "Build" button. After a few false starts (I started with C strings instead of NSStrings in the error message arguments), the code compiled. The linker then complained that it didn't know about CheckbookEntry. So I added both CheckbookEntry and Checkbook to the "Sources" section of the "Testing" target, clicked "Build," and voilà! I saw in the build window:


// ...build stuff...
-[TestCheckbookEntry testEntryAmount] :
Type mismatch -- bad amount; 123 != 123
-[TestCheckbookEntry testEntryAmount] :
Type mismatch -- bad amount; 1.23 != 1.230000
-[TestCheckbookEntry testEntryAmount] :
<3ff3ae14 7ae147ae >' should be equal to
'<3f9d70a4 >' bad amount; 1.23 != 1.230000

The code we saw in the previous section did not cause these errors; that code is correct. These errors were caused by my original code, which looked like this:


- (void) testEntryAmount {
    [entry setAmountInPennies:123];
    STAssertEquals(123, [entry amountInPennies],
                   @"bad amount; 123 != %ld",
                   [entry amountInPennies]);
    STAssertEquals(1.23, [entry amount],
                   @"bad amount; 1.23 != %f",
                   [entry amount]);
}

I simplified the test case by removing all but the first Assert and removing my custom error message. This is what the code looked like.


- (void) testEntryAmount {
    [entry setAmountInPennies:123];
    STAssertEquals(123, [entry amountInPennies],
                   nil);
    // commented-out old versions not shown
}

I clicked "Build" and saw the error message:


-[TestCheckbookEntry testEntryAmount] :
Type mismatch --

The clue I needed was in the phrase "Type mismatch." After a bit of thinking, I looked at the amountInPennies method. Aha! It returns a long, not an int. I changed the first argument of the test to 123L like this:


- (void) testEntryAmount {
    [entry setAmountInPennies:123];
    STAssertEquals(123L, [entry amountInPennies],
                   nil);
    // commented-out old versions not shown
}

I rebuilt the test target, and it worked! I then wrote all the other tests, which worked perfectly the first time. (If you believe that, I've got some dot-com stock options for you.)

Configurable Tests

In order to test much of the code, we need a checkbook object that contains things to find. We could either create the data manually and feed it to the checkbook object or read it from an XML file (CheckbooX stores its data as XML). Let's use the latter method. That way, we can change the test data without having to recompile the code.


- (void) setUp {
    checkbook = [[Checkbook alloc] init];
    [checkbook openFile:TEST_CHECKBOOK_FILE];
}

While we're at it, let's store the expected answers in an XML file, too. Then we can write test methods that don't know the expected answer; they just have to know the name of the answer in the dictionary and the type of the answer. Since we want more than one test class to read an answer file, let's abstract that behavior into a new superclass, TestWithAnswers. The method TestWithAnswers.readAnswersFrom: and the first few answerAs* methods look like this:


-(void)readAnswersFrom:(NSString *)filePath {
    NSArray *contents = [NSArray arrayWithContentsOfFile:filePath];
    NSEnumerator *e = [contents objectEnumerator];
    answers = [[e nextObject] retain];
}

-(NSString *)answerAsString:(NSString *)answerName {
    return (NSString *)[answers objectForKey:answerName];
}

-(int)answerAsInt:(NSString *)answerName {
    return [[answers objectForKey:answerName] intValue];
}

Now we can write test methods that don't have the answer hard-coded in the test. For example, here is the test method TestCheckbook.testBalance:


- (void) testBalance {
    STAssertEquals([self answerAsFloat:
                       @"balance"],
                   [checkbook balance],
                   nil);
    STAssertEquals([self answerAsFloat:
                       @"markedBalance"],
                   [checkbook markedBalance],
                   nil);
}

Final Thoughts

Now when I make a change to CheckbooX, I rebuild the testing target that runs all of the unit tests. Since writing the unit tests for CheckbooX, I feel more confident that any changes I make -- whether they're bug fixes or new features -- will not break the existing code. Though the initial set of tests is sparse, over time I will be adding more tests to CheckbooX that cover more of the code. Any new Mac OS X applications I write will be developed with unit tests from the start.

Testing is good. Just do it.

Resources

The Portland Pattern Repository and WikiWikiWeb top-level pages are deceptively sparse. Browse the main Category page or the Category Testing page. Search for programming terms and you'll come across an amazingly deep ocean of discussion. For example, searching for page titles containing "test" returns 529 pages.

JUnit.org is a good jumping-off point for learning about JUnit and other XUnit frameworks.

Jim Menard is an independent consultant who has developed many open source projects, including the Java GUI report writer DataVision, the first pure-Ruby XML parser NQXML, TwICE (an Information and Content Exchange (ICE) reference implementation), and, of course, CheckbooX.


Return to the Mac DevCenter