Cocoa unit testing with OCUnit and OCMock: Mocking out IBOutlets
I’ve been doing Objective-C (Cocoa) development for OS X at work now for just under a month, and I’m probably going to start pasting some useful snippets here on the blog.
One thing that I had some issues getting to work when I started writing unit tests for my code was IBOutlets for objects that were instantiated through the interface builder. These will only have their bindings setup when initializing the class with loadNibNamed from NSBundle or something similar. This is a bad idea during unit tests since it makes your tests depend on other classes. It is also rather slow.
Instead it’s better to just inject the IBOutlets manually.
The starting point
Let’s pretend I have a class called GameEngine whose interface and implementation looks something like this.
// GameEngine.h
@interface GameEngine : NSObject
{
// Handled by interface builder bindings.
IBOutlet EnemyManager *enemyManager;
}
- (void)updateEnemies;
@end
// GameEngine.m
@implementation GameEngine
- (void)updateEnemies
{
[enemyManager update];
}
@end
If I were to test updateEnemies, I would have to detect if update was called for the enemyManager. If I just initate a GameEngine in my tests like the example below, enemyManager will not be hooked up (due to the lack of reference to the nib file which hooks up the bindings). This means that there’s no way to see what actually goes on inside the class.
One might think that this could be an issue, but it’s actually quite nice once you apply some additional test code.
Injecting the IBOutlet bound object
To setup you IBOutlets in your tests, you simply have to add an additional initializer as a category in your test code. This wont expose any additional initializer in the classes API outside of the tests, and it will allow you to replace the outlet bindings with mocks.
// GameEngineTestCases.m
@interface GameEngine (testing)
- (id)initWithEnemyManager:(EnemyManager*)manager;
@end
@implementation GameEngine (testing)
- (id)initWithEnemyManager:(EnemyManager*)manager
{
self = [self init];
enemyManager = manager;
return self;
}
@end
@implementation GameEngineTestCases
- (void)testUpdateEnemies
{
id mockEnemyManager = [OCMockObject mockForClass:[EnemyManager class]];
[[mockEnemyManager expect] update];
GameEngine *engine = [[GameEngine alloc] initWithEnemyManager:mockEnemyManager];
[engine updateEnemies];
[mockEnemyManager verify];
}
@end
Now you’re safely able to test your class without having to worry about nib files or external classes.
There’s a neat little trick that allows you to set outlets without having to create a special init method. In your example you would do the following:
GameEngine *engine = [[GameEngine alloc] init];
[engine setValue:mockEnemyManager forKey:@"enemyManager"];
More detail in a blog post I wrote a while ago:
http://erik.doernenburg.com/2008/07/testing-cocoa-controllers-with-ocmock/
Heh, yeah that works as well
I think I like that solution slightly more as well. Seems cleaner than using a custom initializer.
edit: Thanks for OCMock btw. Keep up the good work. I hope Xcode will improve its integration with it sooner or later…