Objective-C: NSObject, Part 2

by Michael F. Collins, III August 21, 2009 18:27

In my previous post, I went into more detail about the NSObject class. NSObject is the root class of the Objective-C object model. In this post, I’m going to explore the NSObject class a little deeper and look at the description, isEqual, and hash methods.

If you’re like me and you’re coming from the Java or .NET world, you’re probably familiar with the Object classes that are the root of their class hierarchies. In the .NET world, the System.Object class looks like this:

namespace System
{
    public class Object
    {
        public virtual string ToString();
        public virtual bool Equals(Object other);
        public virtual int GetHashCode();
    }
}

The Object.ToString method returns a string representation of the object. The default implementation of Object.ToString outputs the type of the object. The Equals and GetHashCode methods are used together to compare object identity. Identical objects should return the same value from GetHashCode. If identical objects are compared using Equals, the result should be true.

Looking at the specification for NSObject, or more specifically the NSObject protocol, we can see some similar methods:

@protocol NSObject

- (NSString *)description;
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;

@end

To better understand these methods, I think that we should play with them. Some people would choose to create sample programs to try different experiments. We could do that, but it takes too long in my opinion. Instead, I’m going to choose a different approach and use the integrated support in Xcode for OCUnit, a unit testing framework for Objective-C, to run my experiments. By the way, if you’re new to unit testing or test-driven development, a great use for unit testing frameworks in any language is for experimentation to learn a new platform or API.

I’m going to start off by creating a new test case named NSObjectTests:

#import 

@interface NSObjectTests : SenTestCase
@end

I don’t need to include anything in the public definition of the NSObjectTests class, so I’ll just leave it empty. OCUnit works similarly to how I remember JUnit working (it’s been a while since I’ve done Java development, so please excuse me if this has changed). OCUnit will use reflection to scan the class for methods beginning with the prefix “test.” OCUnit will execute those methods automatically when the test target is built and run.

The first thing that I want to do is see what the description method returns. I’m going to use the NSLog function to output the description to the console. First, I’m going to create a class that I’ll build up throughout this post:

@interface Person : NSObject
@end

@implementation Person
@end

Now, let’s create a Person object and see what we get for description:

@implementation NSObjectTests

- (void)testDescription {
    Person *person = [[Person alloc] init];
    
    NSLog(@"%@", person);

    [person release];
}

@end

In this test case, I am creating a new Person object, outputting the value of the object, and releasing my object (see my last post about memory management). When I run this code, I get the following result:

Obviously, “Person” is the name of the class, just like how .NET does ToString. I am guessing that “0x1063d0” might be the hash code for the object. I modified my test case to output the hash code for the object to confirm:

- (void)testDescription {
    Person *person = [[Person alloc] init];

    NSLog(@"%@", person);
    NSLog(@"%x", [person hash]);

    [person release];
}

When I ran the test again, I got the following output:


1063d0

My assumption was correct. Since I’m dealing with a Person object, I really would like friendlier functionality. I’d like the string representation of the Person object to be the name of the person, so I’m going to change up my Person class to add some properties and an initializer method.

First, I’m going to rehash the three rules of test-driven development that I listed in another post:

  1. You are not allowed to write any production code unless it is to make a failing unit test past.
  2. You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures.
  3. You are not allowed to write any more production code than is sufficient to pass the one failing unit test.

Given these rules, I’ll start by adjusting my unit test. The first thing that I want to do is to create a new person with a first name, middle name, and last name. Then I’ll output the result of the description method. Finally, I’ll verify that the result of the description method matches the full name of the person.

- (void)testDescription {
    Person *person = [[Person alloc] initWithFirstName:@"Michael"
        middleName:@"Francis"
        lastName:@"Collins"];
    [person autorelease];

    NSLog(@"%@", person);

    STAssertEqualObjects(
        @"Michael Francis Collins",
        [person description],
        @"The names do not match.");
}

At this point, I’ve satisfied rule #1, but rule #2 is now violated, because I do have a compilation failure since the initWithFirstName:middleName:lastName method is not declared on Person. I’ll correct that now, and I’ll also define properties for the first name, middle name, and last name.

@interface Person : NSObject {
    NSString *firstName;
    NSString *middleName;
    NSString *lastName;
}

@property (nonatomic, retain) NSString *firstName;
@property (nonatomic, retain) NSString *middleName;
@property (nonatomic, retain) NSString *lastName;

- (id)initWithFirstName:(NSString *)first
    middleName:(NSString *)middle
    lastName:(NSString *)last;

@end

@implementation Person

@synthesize firstName;
@synthesize middleName;
@synthesize lastName;

- (id)initWithFirstName:(NSString *)first
    middleName:(NSString *)middle
    lastName:(NSString *)last {
    if (self = [super init]) {
        self.firstName = first;
        self.middleName = middle;
        self.lastName = last;
    }

    return self;
}

- (void)dealloc {
    [super dealloc];
    [firstName release];
    [middleName release];
    [lastName release];
}

- (NSString *)description {
    NSString *name = [[NSString alloc] initWithFormat:@"%@ %@ %@",
        firstName, middleName, lastName];
    [name autorelease];
    return name;
}

@end

When my unit test runs, I can see in the console that the output for the NSLog function call says “Michael Francis Collins.” My unit test also passes, so I can see that my new description method is working.

The next thing that I want to do is to try the isEqual method on NSObject and see how that works. I’ll create a new test case to try it out and will compare two Person objects that are identical.

- (void)testEquality {
    Person *person1 = [[Person alloc] initWithFirstName:@"Michael"
        middleName:@"Francis"
        lastName:@"Collins"];
    [person1 autorelease];

    Person *person2 = [[Person alloc] initWithLastName:@"Michael"
        middleName:@"Francis"
        lastName:@"Collins"];
    [person2 autorelease];

    BOOL equal = [person1 isEqual:person2];
    STAssertTrue(equal, @"The objects are not equal");
}

When I ran this test, the unit test failed because the isEqual method returned NO. My assumption is that the NSObject implementation of isEqual works like the default Object.Equals implementation in .NET and is doing a comparison by reference. Since I am comparing two different objects, the comparison is failing. Let’s test that out and create a new test method.

- (void)testIdentity {
    Person *person = [[Person alloc] initWithFirstName:@"Michael"
        middleName:@"Francis"
        lastName:@"Collins"];
    [person autorelease];

    BOOL equal = [person1 isEqual:person1];
    STAssertTrue(equal, @"The object is not equal to itself.");
}

I ran this test and the test passed. My assumption is correct. What I need to do now is to override the isEqual method with my own implementation that will compare two Person objects by their values to determine equality.

- (BOOL)isEqual:(id)object {
    if (nil == object) {
        return NO;
    }

    if (self == object) {
        return YES;
    }

    if (![object isKindOfClass:[Person class]]) {
        return NO;
    }

    Person *person = (Person *)object;
    return [firstName isEqual:other.firstName] &&
        [middleName isEqual:other.middleName] &&
        [lastName isEqual:other.lastName];
}

I wrote the isEqual method just like I would have written it in C#. I then build and run my test suite again and, as I expected, all three tests pass. Just to be on the safe side, let’s write a negative test to make sure that it works:

- (void)testInequality {
    Person *person1 = [[Person alloc] initWithFirstName:@"Michael"
        middleName:@"Francis"
        lastName:@"Collins"];
    [person1 autorelease];

    Person *person2 = [[Person alloc] initWithFirstName:@"Meleeka"
        middleName:@"Anjuli"
        lastName:@"Collins"];
    [person2 autorelease];

    BOOL equal = [person1 isEqual:person2];
    STAssertFalse(equal, @"The objects should not have been equal.");
}

In this test, I’m comparing me to my wife. While we’re a team at home, we’re two separate people with different names, so isEqual should return false. When I ran the test suite again, all four tests passed. So isEqual does appear to be working correctly. I should also test the nil case, and the case of comparing different types of objects (both should fail). In the effort of being honest, I actually violated rule #3 by adding that code, so let’s just write those tests for completeness:

- (void)testNil {
    Person *person = [[Person alloc] initWithFirstName:@"Michael"
        middleName:@"Francis"
        lastName:@"Collins"];
    [person autorelease];

    BOOL equal = [person isEqual:nil];
    STAssertFalse(equal, @"The object should not be equal to nil.");
}

- (void)testDifferentTypes {
    Person *person = [[Person alloc] initWithFirstName:@"Michael"
        middleName:@"Francis"
        lastName:@"Collins"];
    [person autorelease];

    BOOL equal = [person isEqual:@"Michael Francis Collins"];
    STAssertFalse(equal, @"The object should not be equal to a string.");
}

These new tests should satisfy the other two checks in the isEqual implementation which compare the object parameter to nil and check to see if object is of kind Person. I ran the tests and they worked.

The hash function is used to generate a unique hash value for the Person object. Learning from my lessons in the Java and .NET world, the hash value should be calculated based on immutable values in the Person object. At this moment, we don’t have any immutable values, so I’ll leave the hash method alone.

One last question that I did have was whether or not my dealloc method was getting called. I didn’t call release on the objects, because the release would have destroyed my objects before doing the test if I had put release before the test. If any of my tests failed, then release would not have been called. Instead, as you’ll notice, I immediately called autorelease after constructing my test objects. I then put an NSLog statement at the start of my dealloc implementation to tell me that the method had been called. I was then able to see in the test execution log that dealloc was being called on all of my objects regardless of whether the test passed or failed, so I don’t believe that I’m leaking any memory in the test cases.

In this post, we explored the description, isEqual, and a little bit of the hash method on NSObject. We created a test class and implemented our own versions of the description and isEqual methods to validate some assumptions of the base NSObject implementation, and looked at how we could re-implement those methods for our subclass. This post also demonstrated creating unit tests using the OCUnit unit test framework that’s integrated into the Xcode development environment.



Tags: , , , , , ,

Objective-C | Unit Testing

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen | Modified by Mooglegiant

Calendar

<<  March 2010  >>
MoTuWeThFrSaSu
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar

What I'm reading now


Add to Technorati Favorites

Disclaimer

The views expressed on this website/blog are the opinions of Michael F. Collins, III, and do not necessarily reflect the views of my employer.