Poking Objective-C with a Testing Stick
I’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.
We’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.
With 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.
Let’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.
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
You 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.
With 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.
Ok, 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.
Right 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.
At 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.