iOS: Test Driving Objective-C Retain & Release, Revisited

Michael found a few problems in the code I presented in iOS: Test Driving Objective-C Retain & Release.

Obviously he tried to use it :-)

Based upon his comments I have revisited the code as you can see it below.

First issue is, that the RetainReleaseMock will simply crash when the verify complains. RetainReleaseMock is no test case and so it is missing the method used to report an issue.

Next issue is, that it doesn’t provide very useful messages. You don’t see if it is complaining about the retain or the release count.

To fix the first issue, Michael moved the assertions back to the test method. Although this is a bit longer than the simple verify, it fixes the first issue and second issue in one step.

Ok, I agree, that’s better so I have revisited my code with some renaming (it is no longer a mock but a spy), a special hamcrest matcher and a noise reducing macro which you may use or not.

Here is the test code (without macro and with macro):

@interface CredentialsTest : GTMTestCase {
}
@end

@implementation CredentialsTest
- (void)testShouldRetainAndReleaseUserAndPassword {
  id user = [[[RetainReleaseSpy alloc] init] autorelease];
  id password = [[[RetainReleaseSpy alloc] init] autorelease];

  [[[Credentials alloc] initWithUser:user password:password] release];

  assertThat (user, isRetained (1));
  assertThat (user, isReleased (1));
  assertThat (password, isRetained (1));
  assertThat (password, isReleased (1));
}

@implementation CredentialsTest
- (void)testShouldRetainAndReleaseUserAndPassword {
  id user = RRSpy;
  id password = RRSpy;

  [[[Credentials alloc] initWithUser:user password:password] release];

  assertThat (user, isRetained (1));
  assertThat (user, isReleased (1));
  assertThat (password, isRetained (1));
  assertThat (password, isReleased (1));
}

@end

In this case I intentionally put everything in a single test and violate the “single assertion (concept) per test” rule. It has everything in one spot and I think I can handle “hidden assertion” issue (I wont see the failures of the later assertions when the first fails). ;-)

Assertion failures are reported like this:

-[CredentialsTest testShouldRetainAndReleaseUserAndPassword] : Expected release count 1, but was 0

The isRetained and isReleased calls are creating hamcrest matchers to make it a little bit easier to read.

Here is the code of the new RetainReleaseSpy, the matcher code (header and implementation) and the matcher factory methods:

// ios
#import <Foundation/Foundation.h>

// test
#import <OCHamcrest/HCBaseMatcher.h>

@interface RetainReleaseSpy : NSObject {
  int refCount;
}

@property (nonatomic, readonly) int actualRetainCount;
@property (nonatomic, readonly) int actualReleaseCount;

- (id)init;
- (id)retain;
- (void)release;

@end


id<HCMatcher> isRetained (int expectedRetainCount);
id<HCMatcher> isReleased (int expectedReleaseCount);

#define RRSpy [[[RetainReleaseMock alloc] init] autorelease]
#import "RetainReleaseSpy.h"

// test
#import <OCHamcrest/OCHamcrest.h>
#import <OCHamcrest/HCDescription.h>


@implementation RetainReleaseSpy

@synthesize actualRetainCount;
@synthesize actualReleaseCount;

- (id)init {
  if ((self = [super init])) {
    actualRetainCount = 0;
    actualReleaseCount = 0;
    refCount = 1;
  }
  return self;  
}

- (id)retain {
  ++refCount;
  ++actualRetainCount;
  return self;
}

- (void)release {
  ++actualReleaseCount;
  --refCount;
  
  if (refCount == 0) {
    [self dealloc];
  }
}

@end


@interface RetainReleaseMatcher : HCBaseMatcher {
  int expectedCount;

  SEL property;
  NSString* info;
}

- (id) initWithExpectedCount:(int)expectedCount property:(SEL)property info:(NSString*)info;

@end


@implementation RetainReleaseMatcher

- (id) initWithExpectedCount:(int)anExpectedCount property:(SEL)aProperty info:(NSString*)anInfo {
  if ((self = [super init])) {
    expectedCount = anExpectedCount;
    property = aProperty;
    info = [anInfo retain];
  }
  return self;
}

- (BOOL) matches:(id)item {
  int actualCount = (int)[item performSelector:property];
  if (actualCount != expectedCount) {
    return NO;
  }
  return YES;
}

- (void) describeTo:(id<HCDescription>)description {
  [description appendText:[NSString stringWithFormat:@"%@ count %d", info, expectedCount]];
}

- (void) describeMismatchOf:(id)item to:(id<HCDescription>)mismatchDescription {
  int actualCount = (int)[item performSelector:property];
  [mismatchDescription appendText:[NSString stringWithFormat:@"was %d", actualCount]];
}

- (void)dealloc {
  [info release];
  [super dealloc];
}   

@end


id<HCMatcher> isRetained (int expectedRetainCount)
{
  return [[[RetainReleaseMatcher alloc] initWithExpectedCount:expectedRetainCount
    property:@selector(actualRetainCount) info:@"retain"] autorelease];
}

id<HCMatcher> isReleased (int expectedReleaseCount)
{
  return [[[RetainReleaseMatcher alloc] initWithExpectedCount:expectedReleaseCount
    property:@selector(actualReleaseCount) info:@"release"] autorelease];
}

Better than before? I think so.

Leave a comment