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.

Advertisements

First Impression of appCode, JetBrains Objective-C IDE

JetBrains just released their first EAP version of a new IDE for Objective-C.

Objective-C ???? Yes, no joke. :-)

The JetBrains page says:

appCode is a new IDE based on IntelliJ platform, for developers building apps for Apple devices such as Macs, iPhones & iPads.”

I don’t do daily programming in Java but If I do java programming I use IntelliJ IDEA. At work with the commercial version and at home with the community edition.

My free time interest right know is iOS development and I have written a couple of articles about iOS stuff on my blog. So I was quite curious to take look at appCode.

I downloaded the archive and started to play around with it.

Here are a couple of things I tried:

Loading an Xcode project did work without problem.

Building and running my app did work out of the box. The only thing was that I had to specifically set the device to iPad. It didn’t pick that up by itself.

Using the typical IDEA run/debug configuration dialog I was also able to configure and run my GTM unit tests. appCode recognizes all the targets from the Xcode project and you can modify the SDK, device and so on.

appCode doesn’t seem handle the test results so far. It complained about 65 errors although only 2 failed. Not sure where it got that number from. The failing tests are not appCodes fault. Double clicking the error also didn’t jump to the source.

Debugging works too. I was able to set breakpoints and step through the code. [Update: 11.04.] A bit strange is that it didn’t honor breakpoints in all methods. I’m unable to reproduce the breakpoints that did not work. I retried multiple times and now it just works. I don’t know what made me believe it did not stop on all breakpoints.

The editor itself works great. You are directly at home if you know IntelliJ IDEA. It offers a similar feature set:

IDEAs common navigation shortcuts, auto completion, inspections, live templates and so on.

At the left of the editor you have small icons that tell you that a method overwrites a base class method and clicking it will jump to its declaration. appCode also marks methods with a small pair of arrows icon to jump from declaration to definition and the other way.

Auto completion works really good. I’ve just used it a short time but it looks like it provides better suggestions (less noise) than Xcode.

Inspections. It’s recognizing unused #imports and variables and creates the IDEA typical yellow lines in the editor scroll bar.

Live Templates work too. For example typing alloc<tab> will result in [[ alloc] init] with the cursor before the alloc.

It also comes with a number of refactorings. I tried rename and extract method without problems.

appCode did open a xib file as xml. It didn’t run Interface Builder. Not sure if it is even possible to run Interface Builder now that it is built into Xcode 4.

It also had some problem to find all the #imported files and complained about undefined symbols (in the editor, building works, I guess it simply runs xcodebuild).

I have created a couple of different source folders in my project to organize the sources. In Xcode I have set a couple of additional #import folders and #import header files relative to them.

appCode didn’t pick up the additional #import folders. You can tell appCode about the source root folder and test source root folder but that did not completely fix this issue. I have three parallel source folders: “Source”, “Source Test” and “Source External” and the external folder contains the GTM, OCMock and OCHamcrest code which appCode did not find.

My first impression (after maybe an hour and a half) is quite good. I think I will spend some more time working with it and see how it feels when doing some real work.

What’s your opinion on appCode?