views:

699

answers:

1

I'm currently trying to learn objective-c using XCode 3.1. I've been working on a small program and decided to add unit testing to it.

I followed the steps on the Apple Developer page - Automated Unit Testing with Xcode 3 and Objective-C. When I added my first test, it worked fine when the tests failed, but when I corrected the tests the build failed. Xcode reported the following error:

error: Test host '/Users/joe/Desktop/OCT/build/Debug/OCT.app/Contents/MacOS/OCT' exited abnormally with code 138 (it may have crashed).

Trying to isolate my error, I re-followed the steps from the Unit Test example above and the example worked. When I added a simplified version of my code and a test case, the error returned.

Here is the code I created:

Card.h

#import <Cocoa/Cocoa.h>
#import "CardConstants.h"

@interface Card : NSObject {
    int rank;
    int suit;
    BOOL wild ;
}

@property int rank;
@property int suit;
@property BOOL wild;

- (id) initByIndex:(int) i;

@end

Card.m

#import "Card.h"

@implementation Card

@synthesize rank;
@synthesize suit;
@synthesize wild;

- (id) init {
    if (self = [super init]) {
        rank = JOKER;
        suit = JOKER;
        wild = false;
    }
    return [self autorelease];
}

- (id) initByIndex:(int) i {
    if (self = [super init]) {
        if (i > 51 || i < 0) {
            rank = suit = JOKER;
        } else {
            rank = i % 13;
            suit = i / 13;
        }
        wild = false;
    }
    return [self autorelease];
}

- (void) dealloc {
    NSLog(@"Deallocing card");
    [super dealloc];
}

@end

CardTestCases.h

#import <SenTestingKit/SenTestingKit.h>

@interface CardTestCases : SenTestCase {
}
- (void) testInitByIndex;
@end

CardTestCases.m

#import "CardTestCases.h"
#import "Card.h"

@implementation CardTestCases

- (void) testInitByIndex {
    Card *testCard = [[Card alloc] initByIndex:13];
    STAssertNotNil(testCard, @"Card not created successfully");
    STAssertTrue(testCard.rank == 0,
                 @"Expected Rank:%d Created Rank:%d", 0, testCard.rank);
    [testCard release];
}
@end
+12  A: 

I've encountered this numerous times myself, and it's always annoying. Basically, it usually means that your unit tests did crash, but doesn't help isolate the error. If the unit tests produced output before crashing (open Build > Build Results) you can usually at least get an idea of what test was running when the problem occurred, but this alone usually isn't too helpful.

The best general suggestion for tracking down the cause is to debug your unit tests. When using OCUnit, this is unfortunately more complex than selecting Run > Debug. However, the same tutorial you're using has a section near the bottom titled "Using the Debugger with OCUnit" which explains how to create a custom executable in Xcode to execute your unit tests in a way that the debugger can attach to. When you do, the debugger will stop where the error occurred, instead of getting the mysterious "code 138" when everything goes down in flames.

Although I may not be able to guess exactly what's causing the error, I do have a few suggestions...

  • NEVER, EVER autorelease self in an init method — it violates retain-release memory rules. That alone will lead to crashes if the object is released unexpectedly. For example, in your testInitByIndex method, testCard comes back autoreleased — therefore, [testCard release] on the last line == guaranteed crash.
  • I'd suggest renaming your initByIndex: method to initWithIndex:, or even switching to initWithSuit:(int)suit rank:(int)rank so you can pass both values, instead of a single int (or an NSUInteger, which would eliminate testing for < 0) that you have to handle.
  • If you really want a method that returns an autoreleased object, you can also create a convenience method like +(Card*)cardWithSuit:(int)suit rank:(int)rank instead. This method would just return the result of a one-line alloc/init/autorelease combination.
  • (Minor) Once you're done debugging, get rid of the dealloc that just calls to super. If you're trying to find memory that's never deallocated, it's much easier to find using Instruments anyway.
  • (Niggle) For your test method, consider using STAssetEquals(testCard.rank, 0, ...) instead. It tests the same thing, but any resulting error is a bit easier to understand.
  • (Trivial) You don't have to declare unit test methods in the @interface. OCUnit dynamically runs any method of the format -(void)test... for you. It doesn't hurt to declare them, but you'll save yourself some typing if you just omit them. On a related note, I usually have only a .m file for unit tests, and put the @interface section at the top of that file. This works great since nobody else needs to include my unit test interface.
  • (Simplicity) Unless you subclass CardTestCases, it is simpler to just eliminate the .h file and put the @interface at the top of the .m file instead. Header files are necessary when multiple files need to include the declarations, but this is usually not the case with unit tests.

Here is what the test file might look like with these suggestions:

CardTest.m

#import <SenTestingKit/SenTestingKit.h>
#import "Card.h"

@interface CardTest : SenTestCase
@end

@implementation CardTest

- (void) testInitWithIndex {
    Card *testCard = [[Card alloc] initWithIndex:13];
    STAssertNotNil(testCard, @"Card not created successfully");
    STAssertEquals(testCard.rank, 0, @"Unexpected card rank");
    [testCard release];
}
@end
Quinn Taylor
autorelease was the culprit. I miss typed the filenames writing the question so tip 4 was not an issue. tip 2 - my code does contain other init functions including the suggested one. I wanted to limit my code as much as possible in an attempt to isolate the error.
Joe
Glad that helped. Removed the file naming tip since it was a typo. You're smart to have posted only the code that caused the error. :-)
Quinn Taylor