everburning

home quotes

Poking Objective-C with a Testing Stick

26 Oct 2009

4012929364_54af446306_bI've wandered back into Objective-C coding land recently. After spending so much time doing Ruby) work I've gotten used to writing unit tests and using mock objects. To that end, I spent a bit of time figuring out OCUnit (built into XCode) and OCMock. I figured I'd write some of this down so I don't forget for the next time I try to set this all up.

Build Results 1We're going to work with a simple Cocoa Application that connects to an external web resource, in this case, Google Reader authentication. We'll setup the testing bundle to run when our main application is built and create a mock object so we don't hit the external resource on every test run. I'm going to be using XCode version 3.2 and create a Cocoa Application called UnitTesting. If you build and run this application you should see an empty window on screen. If you bring up the Build Results window (Build -> Build Results) you'll see some information about the current build.

Add Unit Test TargetWith our app setup, we'll start integrating the unit tests. Click on the Targets item in the project folder. Select Add -> New Target. Select Unit Test Bundle and press __Next (Note, make sure you're in the Cocoa and not the Cocoa Touch section when selecting Unit Test Bundle). I named my target Unit Tests and hit Finish.

At this point if you right click on the UnitTesting target and select Build "UnitTesting" everything should work correctly. If you right click on Unit Tests and select Build "Unit Tests" you should receive a build failure.

Add Test GroupLet's add some tests. The first step is to create a new group to hold our test files. Right click on the UnitTesting item in the project draw and select Add -> New Group. Name the new group Tests. Now, right click on the Tests group and select Add -> New File.... You'll want to add an Objective-C test case class (Note, make sure you're in the Cocoa Class section of the new dialog and not the Cocoa Touch Class section or you'll get an iPhone test class). Name the test GReaderTest.m and de-select the Also create "GReaderTest.h" option. The reason for this is that the headers are usually empty so there is no point in creating the extra file. You'll also want to make sure you have the Unit Tests target selected.Add Test
Case

When working with OCUnit you need to name each of your test classes SomethingTest. The trailing Test is required. In a similar vein, each of the tests themselves needs to start with test. So, something like testAuthentication.

Since we didn't create a header file we'll need to setup the interface for the test in the .m file.

#import <SenTestingKit/SenTestingKit.h>

@interface GReaderTest : SenTestCase
@end

@implementation GReaderTest
@end

Bundle Build FailureYou should be able to build the Unit Tests target now and have the build succeed. If the build doesn't succeed, and you see a message about UIKit then you selected the Cocoa Touch unit test bundle instead of the Cocoa unit test bundle.

Adding dependenciesWith the unit tests building we can add them into our main build as a dependency. Right click on the UnitTesting target in the project drawer and select Get Info. In the General section add a new Direct Dependency for the Unit Tests target.

Now, when you press apple-B to build the project you should see your unit tests executed before the main build phase.

Build Results 2Ok, with everything setup we can start testing. First step, each test in this set will be using our GReader object. So, we'll add setUp and tearDown methods that will be executed before and after each test, respectively.

#import <SenTestingKit/SenTestingKit.h>
#import "GReader.h"

@interface GReaderTest : SenTestCase {
    GReader *gr;
}
@end

@implementation GReaderTest
- (void)setUp {
    gr = [[GReader alloc] init];
}

- (void)tearDown {
    [gr release];
}
@end

This, of course, won't execute as we haven't created our GReader object yet. Let's do that now.

Right click on the Classes group and select Add -> New File.... Add a new Objective-C class which is a subclass of NSObject. Call this new class GReader. The new class should be attached to both our main target and the Unit Tests target.

Now, for our little app, the first thing we'll need to do is authenticate with Google Reader. There is a really good document on the Reader API from the pyrfeed project. We'll need to post some specific data to a given end point and parse the response.

For our unit tests, we don't actually want to hit the Google endpoint. There is too much time involved and we want our unit tests to be fast. So, we'll need to do some mocking in order to verify the call is happening, but not actually make the call itself.

For this we'll use OCMock. I'm going to add the test first and then we'll add the OCMock.framework into the project. First, we need to import OCMock. This is done by adding #import <OCMock.h> to the beginning of the GReaderTest.m file.

The test is defined as follows.

- (void)testAuthentication {
    id mock = [OCMockObject partialMockForObject:gr];
    [[[mock stub] andCall:@selector(fakeAuthenticationPost:)
                 onObject:self] post:[OCMArg any]];

    [gr authenticateWithUsername:@"dan" password:@"password"];
}

- (NSString *)fakeAuthenticationPost:(NSString *)request {
    NSArray *d = [request componentsSeparatedByString:@"&"];

    STAssertTrue([d containsObject:@"Email=dan"], @"Username not set correclty into request");
    STAssertTrue([d containsObject:@"Passwd=password"], @"Password not set correctly in request");

    return @"SID=mysid\nLSID=mylsid\nAuth=myauth";
}

As you can probably tell, we've added two methods. There is only one test, testAuthentication but we needed a helper function to deal with the mock post call. Let's take a look at what's going on in these methods. We know that the GReader object will be making a call that we want to mock. So, we need to create a partial mock object that sits around our GReader object. This is done as: id mock = [OCMockObject partialMockForObject:gr];. With the mock created, we can stub out specific methods of the GReader object, this is called Method Swizzling in Objective-C land.

In order to swizzle the method we have [[[mock stub] andCall:@selector(fakeAuthenticationPost:) onObject:self] post:[OCMArg any]];. So, we create a new stub on our mock object. We then tell the stub to call the fakeAuthenticationPost defined in the current object whenever the post method is called on the GReader object. Our post method takes one parameter so we specify [OCMArg any] to allow any argument through.

Finally, we call the authenticateWithUsername:password: on the GReader object to kick off the authentication.

The second method we defined, fakeAuthenticationPost, will be called instead of the post method in the GReader object. To that end, it will receive the same parameter, the NSString that is pass to post. To be on the safe side, I'm verifying that we're properly passing the required email and password fields in the string. I then fake some return data that is similar to a successful response from Google.

If we try to build the project at this point we're going to get a lot of errors. First, since we haven't added the OCMock framework and second, we haven't created the authenticate or post methods for our GReader object.

First things first, let's get the OCMock framework setup. To do that, you'll need to download OCMock. You can grab the .dmg file off the OCMock pages. When you extract the archive you'll see the OCMock.framework and the source directories. In order to keep everything in Git, I created a Framework directory in my UnitTesting directory. I then copied the OCMock.framework directory into this new Framework directory.

You could also install OCMock in the /Library/Frameworks directory but I like having it in Git as not all the developers may have OCMock installed.

Back in XCode, we need to add the framework to our project. Right click on the Frameworks group in the project directory. Select Add -> New Group. Name the new group Testing Frameworks. Note, this isn't required, I just like the separation it provides. Finally, right click on Testing Frameworks and select Add -> Existing Frameworks... then press the Add Other... button. Navigate to the OCMock.framework directory you just copied into Frameworks directory and press Add. Make sure the new framework is hooked up to the Unit Tests target. Right click on the framework and hit Get Info in the general tab you can verify the targets.

Building the project at this point will get some warnings about missing functions and a crash executing the unit tests. In order to fix the crash we need to setup the build to copy the OCMock framework into our build directory. I'm not entirely sure why this is needed, something about one of the paths set in the framework, but it does get things working.

Copy Build PhaseRight click on the Unit Tests target and select Add -> New Build Phase -> New Copy Files Build Phase.

You need to set the Destination to Absolute Path and the Full Path to $(BUILT_PRODUCTS_DIR). Once the Copy Files phase is created drag the OCMock.framework from the Test Frameworks group into the copy phase. The copy phase needs to be placed between Compile Sources and Link Binary With Library phases.

Built Products dirAt this point, we should be able to execute our build again and we'll get a failure [NSProxy doesNotRecognizeSelector:post]. This is good. This means our mock is setup correctly and is trying to hook into our non-existant post method.

Let's go create a couple methods so things compile correctly. First we update the GReader.h file as follows.

@interface GReader : NSObject
- (void)authenticateWithUsername:(NSString *)username password:(NSString *)password;
- (NSString *)post:(NSString *)request;
@end

And the implementation.

#import "GReader.h"
@implementation GReader
- (void)authenticateWithUsername:(NSString *)username password:(NSString *)password {
    [self post:[NSString stringWithFormat:@"Email=%@&Passwd=%@", username, password]];
}

- (NSString *)post:(NSString *)request {
    return @"My post data";
}
@end

With that in place our build should succeed. You can make sure that things are working correctly by modifying [gr authenticateWithUsername:@"dan" password:@"password"]; to something similar to [gr authenticateWithUsername:@"stan" password:@"password"]; and you should see a build failure.

That's it. We have the build system setup and the mock objects hooked up. We can now continue down our TDD path. You can see an example project that I've uploaded to GitHub.

Tags — Cocoa, Computers, OCMock, OCUnit, Objective-C, Programming, and Testing