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. :-)

7 thoughts on “iOS: Test Driving Objective-C Retain & Release

  1. iOS: Test Driving Objective-C Retain & Release, Revisited « Software Noise

  2. Well, I don’t personally find that a line like:

    assertThatInt(mockThing.retainCount, equalToInt(expectedRetainCount));

    in a test is very noisy. It is longer than your verify lines, but, to me, it’s clearer. And I would generally only test either the retain count OR the release count in a single test, otherwise I can’t really tell which method is doing what.

    However, if you don’t like this, you could write a custom Hamcrest matcher. I haven’t done this with OCHamcrest (I’m still new to iOS programming) but I used to do that a fair bit in Java. It’s fairly straightforward and can DRY up your test code substantially.

  3. Hi Michael,

    I have fixed the #imports.

    Your second issue happens to me too. I just didn’t know how to fix it and other tests are sometimes crashing too when using OCMock… so I’m used to manually locating the bad test which is usually the one I currently working on…

    Your hint about failWithException: is a great help. Deriving from GTMTestCase instead of NSObject does fix the crash. Too bad XCode now jumps to the RetainReleaseMock and not in test where it is used.

    How do you assert in the test without producing too much noise (that and making it behave similar to OCMOck was my reason to move it into the verify)?

    Thanks for your feedback :)

  4. Thanks very much for this! However, there seem to be a couple of problems with the code. Well, at least for me. The first is that the #import lines are truncated (probably due to angle brackets, I expect). The second issue I encountered may be a function of the testing kit I am using. I was assuming that “assertThatInt” is a Hamcrest macro, in which case, I think it needs to run from a class with an “failWithException:” method (e.g. SenTestCase or some descendant). RetainReleaseMock doesn’t have this method so a “does not respond” exception is thrown instead of the usual “test failed” exception. But perhaps you have a different/custom version of Hamcrest, or some custom macro set, that uses a different mechanism to register failures.

    My solution was to remove the expected count properties and test the retain and/or release counts directly in the test method. I think that makes the test clearer as well. But I really appreciate your providing the idea!

  5. I have shifted away from OCMock to hand-rolled mocks much like your example. To assist in writing robust hand-rolled mocks, I have creating a framework called OCHandMock that you may find helpful. Basically, you could replace your expected/actual integer counters with HMExpectationCounter.

    • Hi Jon,

      sounds interesting, I will try it. :-)

      Wow, google just reports a single hit for OCHandMock!

      So far I’m using OCMock for most of my mocks. I have only a couple of hand crafted mocks where the setup got to complicated (that is you don’t understand it anymore after a couple of minutes ;-) with OCMock.

      I general I notice that my mocks tend to get smaller and simpler with more testing experience and now usually have just a single expect. More experience will tell if that is the right way…

Leave a comment