iOS: Test Driving Objective-C Retain & Release

Does it make sense to write unit test for retain & release for my classes?

If I strictly follow Uncle Bob’s TDD Rules then yes. I have to write some test code that will make me add the retain & release cycle for an object.

Even if I don’t strictly follow the rules it may make sense… at least it is a reminder to add the release. Which I forget from time to time.

But finally it is your decision if you test it or not. Anyway, I’d like to talk about how would I do it?

First I tried to achieve it with OCMock but that did not work at all. Fiddling with retain and release on objects out of your control is, let’s say, one on the easiest way to make your test code crash ;-)

So I created a hand made mock called RetainReleaseMock that tracks the retain & release calls.

Here is a test that demonstrates how I use it. It tests that my Credentials object properly retains & releases the user and password NSStrings passed to the initWithUser:password: method.

@interface CredentialsTest : GTMTestCase {
}
@end

@implementation CredentialsTest

- (void)testShouldRetainAndReleaseUserAndPassword {
 id user = [[[RetainReleaseMock alloc] init] autorelease];
 id password = [[[RetainReleaseMock alloc] init] autorelease];

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

 [user verify];
 [password verify];
}

@end

First I create a RetainReleaseMock for the user and password string. Then I create the Credentials object (passing the two mocks) to trigger the retain and directly release it again to trigger the release.

The final step is to verify that retain and release was called on each mock.

That’s it.

Note that you can use [autorelease] to get the mock properly deallocated.

Here is the code of the RetainReleaseMock:

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

@interface RetainReleaseMock : NSObject {
  int refCount;

  int expectedRetainCount;
  int expectedReleaseCount;
  int actualRetainCount;
  int actualReleaseCount;
}

- (id)init;
- (id)initWithRetain:(int)retainCount release:(int)releaseCount;

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

@end

#import "RetainReleaseMock.h"

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

@implementation RetainReleaseMock

- (id)init {
  return [self initWithRetain:1 release:1];
}

- (id)initWithRetain:(int)retainCount release:(int)releaseCount {
  if ((self = [super init])) {
    expectedRetainCount = retainCount;
    expectedReleaseCount = releaseCount;
    actualRetainCount = 0;
    actualReleaseCount = 0;
    refCount = 1;
  }
  return self;
}

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

- (void)release {
  ++actualReleaseCount;
  --refCount;

  if (refCount == 0) {
    [self dealloc];
  }
}

- (void)verify {
  assertThatInt (actualRetainCount, equalToInt (expectedRetainCount));
  assertThatInt (actualReleaseCount, equalToInt (expectedReleaseCount));
}

@end

In case you wonder what the second init method is for:

I have added it to test retain & release on an AppDelegate where I cannot pass the dependencies directly to the AppDelegate (.. because it is created by “iOS”). The AppDelegate calls a (global) factory object that I have stubbed (with OCMock) to return the RetainReleaseMock. The factory stub retains my mock too so I have to adjust the expected retain count.

The test then looks like this:

- (void)testReleasesCoreData {
  // extra retain from appFactory stub.
  id coredata = [[[RetainReleaseMock alloc] initWithRetain:2 release:1] autorelease];

  id appFactory = Stub (AppFactory);
  [[[appFactory stub] andReturn:controller] mainViewController];
  [AppFactory setFactory:appFactory];

  [delegate application:nil didFinishLaunchingWithOptions:nil];
  [delegate release];

  [coredata verify];
  [ReadmeAppFactory reset];
}

If you have a better suggestion to handle this situation, I would like to hear it. :-)