Dependency Injection, iOS and You
iOSDependency injection refers to the design principle of telling a class which other objects its instances should work with, improving the flexibility with which...
Mock objects are used by many developers when they’re using test-driven development to design their systems, but what is it? And what are all these subtypes like partial mocks and nice mocks? Are mock objects usually nasty but impartial? Let’s take a look, using examples from the OCMock framework for Objective-C testing.
There are two different problems we can solve with mock objects. The first (and the one they’re designed for) arises when we’re using test-driven development to drive the design of a class. Imagine the scene: you’ve built your first test, which tells you something about the API for your first class. Your test calls a method on your new class, and you know that in response, the object should grab some information from one of its collaborators. The issue is that this collaborator doesn’t exist yet, and you don’t want to put aside the test you’re already writing to start designing out that class instead.
You can create a mock object to stand in for this nascent collaborator. The mock lets you express the expectation that the object you’re testing calls through to the collaborator, and, if you need it to, it can also return a value that your test controls. Your test can verify that the methods you expect are actually called, with the test failing if that doesn’t happen.
In this scenario, the mock object is acting like a VCR, but without the chunky 1980s styling and the mangled ribbons of tape. During your test, the mock records every message you sent to it. Then you ask it to play that back, and compare it with the list of messages you wanted to send. Just like with a VCR, if you were expecting Gremlins 2 but actually recorded the news and the first half of an episode of Cheers, that’s a disappointing failure.
The key part is that you don’t actually need to build the collaborating object. In fact, you don’t need to worry at all yet about how it will be implemented. All that matters is that you express the messages it responds to, so that the mock object can test whether they’re sent. In effect, the mock lets you say, “I know that at some point I’ll want this, but I don’t want to be distracted by thinking about it.” It’s like a to-do list for TDDers.
Let’s look at an example. Imagine that Big Nerd Ranch has identified a gap in the market for museum inventory management apps. Museums have these huge collections of artefacts, and they need to be able to see what they’ve got, and organise it to work out what to put in their galleries, which might be laid out by theme, country, era and so on. A simple requirement for an app to look after this inventory might be:
As the curator, I want to see all of the artefacts in the collection so that I can tell a story to our visitors.
I’ll write a test that shows that the inventory can be asked for its list of all artefacts. There’ll be some other object that stores all the artefacts on disk, but I don’t want to worry yet how that works, so I’ll just create a mock for the store interface. My test looks like this:
@implementation BNRMuseumInventoryTests
- (void)testArtefactsAreRetrievedFromTheStore
{
//Assemble
id store = [OCMockObject mockForProtocol:@protocol(BNRInventoryStore)];
BNRMuseumInventory *inventory = [[BNRMuseumInventory alloc] initWithStore:store];
NSArray *expectedArtefacts = @[@"An artefact"];
[[[store expect] andReturn:expectedArtefacts] fetchAllArtefacts];
//Act
NSArray *allArtefacts = [inventory allArtefacts];
//Assert
XCTAssertEqualObjects(allArtefacts, expectedArtefacts);
[store verify];
}
@end
To make that test compile, I have to create the BNRMuseumInventory
class and its -initWithStore:
and -allArtefacts
methods.
@interface BNRMuseumInventory : NSObject
- (id)initWithStore:(id <BNRInventoryStore>)store;
- (NSArray *)allArtefacts;
@end
@implementation BNRMuseumInventory
- (id)initWithStore:(id <BNRInventoryStore>)store
{
return nil;
}
- (NSArray *)allArtefacts
{
return nil;
}
@end
I also have to define the BNRInventoryStore
protocol and its -fetchAllArtefacts
method, but I don’t need to implement them yet. Why did I define this as a protocol, and not as another class? For flexibility: I know what messages I want to send to a BNRInventoryStore,
but I don’t yet need to worry about how it does them. Using a protocol here lets me be completely flexible about how the store is implemented, as it can be any type of class, as long as it responds to the messages I care about.
@protocol BNRInventoryStore <NSObject>
- (NSArray *)fetchAllArtefacts;
@end
Now there’s enough information for the compiler to compile and run the test, but it doesn’t pass yet.
Test Case '-[BNRMuseumInventoryTests testArtefactsAreRetrievedFromTheStore]' started.
/Users/leeg/BNRMuseumInventory/BNRMuseumInventory Tests/BNRMuseumInventoryTests.m:91: error: -[BNRMuseumInventoryTests testArtefactsAreRetrievedFromTheStore] : ((allArtefacts) equal to (expectedArtefacts)) failed: ("(null)") is not equal to ("(
"An artefact"
)")
<unknown>:0: error: -[BNRMuseumInventoryTests testArtefactsAreRetrievedFromTheStore] : OCMockObject[BNRInventoryStore]: expected method was not invoked: fetchAllArtefacts
// snip more output
The assertion in the test has detected that the expected collection of artefacts was not returned, and the mock object has failed verification as the -fetchAllArtefacts
method was not called. Fixing both of these problems gets us to a passing test.
@implementation BNRMuseumInventory
{
id <BNRInventoryStore> _store;
}
- (id)initWithStore:(id <BNRInventoryStore>)store
{
self = [super init];
if (self)
{
_store = store;
}
return self;
}
- (NSArray *)allArtefacts
{
return [_store fetchAllArtefacts];
}
@end
The other way in which we can use mock objects is to investigate integration with external code, such as Apple’s frameworks or third-party libraries. The mock object can remove all of the complexity associated with using the framework, so the test doesn’t need to create a full-blown environment just to ensure a small part of our app’s connection to that environment. This use of mock objects follows a test pattern called the Humble Object.
Extending the VCR analogy, we don’t get to design our interaction with a framework class, but we do want to check that we follow their rules correctly. If you’ve bought a VHS recorder, you don’t get to decide what type of tapes to push in. You have to use VHS tapes because the manufacturer made that decision for you. We can tell our mock object to expect a VHS tape, and to fail if we give it a Betamax tape.
Returning to our museum example, the first visible screen when the app is launched should be the list of all the museum’s artefacts. This can be arranged using UIKit by setting the window’s root view controller. Setting up the whole window environment for this test could be slow and complicated, so let’s just replace the window with a mock object.
@implementation BNRAppDelegateTests
- (void)testFirstScreenIsTheListOfAllArtefacts
{
BNRAppDelegate *appDelegate = [[BNRAppDelegate alloc] init];
id window = [OCMockObject mockForClass:[UIWindow class]];
appDelegate.window = window;
[[window expect] setRootViewController:[OCMArg checkWithBlock:^(id viewController) {
return [viewController isKindOfClass:[BNRAllArtefactsTableViewController class]];
}]];
[appDelegate application:nil didFinishLaunchingWithOptions:nil];
[window verify];
}
@end
To make this test pass, implement the app delegate method.
@implementation BNRAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options
{
self.window.rootViewController = [[BNRAllArtefactsTableViewController alloc] initWithStyle:UITableViewStyleGrouped];
return YES;
}
@end
There’s another requirement we have to satisfy in launching a UIKit app, which is that the window containing the initial view controller’s view must be made key and visible. We can add a test that expresses that requirement. Notice that because both this test and the prior test use the same objects, the construction can be factored into a setup method.
@implementation BNRAppDelegateTests
{
BNRAppDelegate *_appDelegate;
id _window;
}
- (void)setUp
{
_appDelegate = [[BNRAppDelegate alloc] init];
_window = [OCMockObject mockForClass:[UIWindow class]];
appDelegate.window = _window;
}
- (void)testWindowIsMadeKeyAndVisible
{
[[_window expect] makeKeyAndVisible];
[_appDelegate application:nil didFinishLaunchingWithOptions:nil];
[_window verify];
}
- (void)testFirstScreenIsTheListOfArtefacts
{
[[_window expect] setRootViewController:[OCMArg checkWithBlock:^(id viewController) {
return [viewController isKindOfClass:[BNRAllArtefactsTableViewController class]];
}];
[_appDelegate application:nil didFinishLaunchingWithOptions:nil];
[_window verify];
}
@end
Now we have a difficult problem. The new test fails for two reasons: the expected -makeKeyAndVisible
message is not being sent, and an unexpected message setRootViewController:
is being sent. Adding the -makeKeyAndVisible
message in -[BNRAppDelegate application:didFinishLaunchingWithOptions:]
means that both tests fail, because now the mock window is receiving one unexpected method in each test.
Nice mocks fix that. A nice mock records the messages it receives, just like a regular mock, but it doesn’t worry about receiving messages that it wasn’t told to expect. It’s like saying, “I want to record that episode of Star Trek: Voyager, but I don’t mind if you caught the weather forecast before it.” It just ignores the extra messages, and doesn’t consider their presence as a need to fail the test.
We can change the test’s mock window to be a nice mock in -setUp
.
- (void)setUp
{
_appDelegate = [[BNRAppDelegate alloc] init];
_window = [OCMockObject niceMockForClass:[UIWindow class]];
appDelegate.window = _window;
}
Now it’s possible to change the app delegate to make both tests pass.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options
{
self.window.rootViewController = [[BNRAllArtefactsTableViewController alloc] initWithStyle:UITableViewStyleGrouped];
[self.window makeKeyAndVisible];
return YES;
}
Sometimes you don’t need to replace all of an object’s behaviour with a mock. You just want to stub out a method to remove some dependency or complex behaviour, the result of which will be used in the method you do want to test. You could create a subclass and override the complex method with your stub, but it’s easier to use a partial mock. Partial mocks act as proxies to real objects, intercepting some messages but using the real implementation for messages they weren’t told to replace.
Back in our museum inventory app, curators need to filter the collection of artefacts by country of origin. That means looking at the list of all artefacts and applying some test to those objects, and we made the -allArtefacts
method communicate with a store object. That’s not something we need to worry about in this test: we want to concentrate on the filtering, without repeating what we’ve already done in the collaboration with the store. Using a partial mock of the inventory object lets us stub out that part of the class. Writing this test also drives out some of the design of the artefact model.
@implementation BNRMuseumInventoryTests
{
BNRMuseumInventory *_inventory; //created in -setUp
}
//...
- (void)testArtefactsCanBeFilteredByCountryOfOrigin
{
id romanPot = [OCMockObject mockForProtocol:@protocol(BNRArtefact)];
[[[romanPot stub] andReturn:@"Italy"] countryOfOrigin];
id greekPot = [OCMockObject mockForProtocol:@protocol(BNRArtefact)];
[[[greekPot stub] andReturn:@"Greece"] countryOfOrigin];
id partialInventory = [OCMockObject partialMockForObject:_inventory];
[[[partialInventory stub] andReturn:@[romanPot, greekPot]] allArtefacts];
NSArray *greekArtefacts = [partialInventory artefactsFromCountry:@"Greece"];
XCTAssertTrue([greekArtefacts containsObject:greekPot]);
XCTAssertFalse([greekArtefacts containsObject:romanPot]);
}
@end
In the above test, I used OCMock’s -stub
method instead of its -expect
method. This tells the mock to handle the message and return the specified value (if appropriate), but doesn’t set up the expectation of the message being sent that the test would later verify. I can tell if the code is working based on what’s returned by the -artefactsFromCountry:
method; I don’t need to worry about how it got there (although if you’re worried about hard-coding some cheat like always returning the last object in the collection, you could simply add more tests).
This test tells us something about the BNRArtefact
protocol.
@protocol BNRArtefact <NSObject>
- (NSString *)countryOfOrigin;
@end
And now the -artefactsFromCountry:
method can be built.
- (NSArray *)artefactsFromCountry:(NSString *)country
{
NSArray *artefacts = [self allArtefacts];
NSIndexSet *locationsOfMatchingArtefacts = [artefacts indexesOfObjectsPassingTest:^(id <BNRArtefact> anArtefact, NSUInteger idx, BOOL *stop){
return [[anArtefact countryOfOrigin] isEqualToString:country];
}];
return [artefacts objectsAtIndexes:locationsOfMatchingArtefacts];
}
Mock objects help you to focus when you’re building applications with test-driven development. They let you concentrate on the test you’re working on now, while deferring decisions about objects you haven’t built yet. They let you concentrate on the parts of the object you’re testing, ignoring the things you’ve already tested or haven’t yet got around to testing. They also let you concentrate on your own code, replacing complicated framework classes with simple stand-ins.
And if you’ve been following the VCR analogy with your real video cassette recorder, it’s probably old enough that you can donate it to the museum where it will find a comfortable home, and a place in the inventory app we’ve just written.
Dependency injection refers to the design principle of telling a class which other objects its instances should work with, improving the flexibility with which...
The Universe is a big place. For someone who's keen on learning new things and absorbing information, that is both a blessing and a...