Despite what some people think, TDD is alive and well. Fast running tests are awesome and breaking dependencies is the key to fast, isolated unit tests. Reliable tests give us the confidence to aggressively clean our code and add new features.
In iOS and OS X it is common to use singleton or singleton style service objects for app wide functionality. Apple provides several of these singleton service objects that are used in most applications. [NSUserDefaults standardUserDefaults];
and [NSNotificationCenter defaultCenter];
for example.
Both NSUserDefaults
and NSNotificationCenter
are notoriously painful to isolate in unit tests. NSUserDefaults
is challenging because it writes to disk and has the potential of poluting the real running application after the test suite has run and we are back to using the app in the simulator. NSNotificationCenter
introduces the possibility of test pollution due to it’s global nature. Wouldn’t it be nice if we had a system to seemlessly inject fake versions of these in our tests?
We do.
At GoSpotCheck we have a mechanism for injecting all of Apple’s as well as all of our own singleton style dependencies. When running in the simulator or on a device the real service objects are injected. When the test target is running fake service objects are injected.
The rest of this post will detail out our approach to dependency injection. To keep things easy to understand the example is intentionally simplistic. The sample project Injections is not organized like our production application and the test suite is XCTest in order to reduce the amount of setup needed to try it out. We use Cedar and Expecta at GoSpotCheck.
The example below shows how to dependency inject a fake AFNetworkReachabilityManager
from Mattt Thompson’s fantastic AFNetworking library. AFNetworking is a great network request library for iOS and OS X.
AFNetworkReachabilityManager
provides hooks for checking on, and being notified of a device’s internet connectivity. To unit test online/offline conditional behavior in an app we need to be able control (or fake) the AFNetworkReachabilityManager's
reported connection status. This example may seem like a lot of code is required to fake the connection status. That using a mocking framework like OCMock might be simpler. It would be. For one off stubbing and mocking OCMock is great. However, in a large production application fakes are usually a better choice in my opinion. Application state is more straightforward with a system of injected fakes than one off mocking, especially for system wide service objects. These techniques are especially useful when unit testing true service objects that make network requests and return deserialized model objects.
Injections is a simple application that displays the device’s connection status when launched. Follow the setup below to see it in action.
Setup
1
|
|
We’ll be looking at a handful of classes and categories.
Environment
is the class that initializes all the services. In a real application you’d see properties for a NSNotificationCenter
, NSUserDefaults
and all other singleton style service objects here. These will be referenced by the UIViewController+Injections
category and made available to all UIViewControllers
that want access.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
The key part to take note of is -initializeServices
. We call [self isTestEnvironment]
to determine if we are in the test environment. If - (BOOL)isTestEnvironment
returns true then we initialize fake services, otherwise we initialize the real reachabilityManager
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
The magic happens with [self respondsToSelector:@selector(initializeFakeServices)]
. In the non-test target this will return false. In the test target it will return true because of the Environment+Fake
category imported in ViewControllerTests.m
.
1 2 3 4 5 6 7 8 9 10 11 |
|
Environment+Fake
sets [FakeReachabilityManager sharedManager]
as the value for self.reachabilityManager
. We’ll see below that ViewController
will use the fake version of self.reachabilityManager
in the test target while using the real reachabilityManager
in the non-test target.
The UIViewController+Injections
category is in charge of defining methods that returns each of the services we want to expose to our view controllers. In this case reachabilityManager
is returned from the Environment
singleton object. This method will return either the real or fake version of the reachabilityManager
depending on the target.
1 2 3 4 5 6 7 8 |
|
ViewController
uses the reachabilityManager
to determine what text to display to the user. If self.reachabilityManger.networkReachabilityStatus
is reachable the label’s text is set to “online”, if not then it is set to “offline”. Take note of the @property (strong, nonatomic) AFNetworkReachabilityManager *reachabilityManager;
declaration in the header as well as it’s corresponding @dynamic reachabilityManager;
in the implementation file. All UIViewController
subclasses have access to the UIViewController+Injections
category methods which allows them to be set as properties on each ViewController
subclass and dynamically evaluated at runtime.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
The final two pieces of the puzzle are in our test suite. ViewControllerTests.m
and FakeReachabilityManager
take care of the rest.
First, FakeReachabilityManager
provides access to toggling the reported connection status as online or offline.
1 2 3 4 5 6 7 8 |
|
By calling -setOffline
or -setOnline
we’re able to override - (AFNetworkReachabilityStatus)networkReachabilityStatus
having it return the value of self.fakeStatus
set in the -setOffline
or -setOnline
calls.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Finally, we make use of our FakeReachabilityManager
in ViewControllerTests.m
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
By toggling our reachabilityManager's
online status through the fake we are able to write assertions about the label’s text for both online and offline states.
Summary
Environment
handles the setup of service objects.Environment+Fake
sets fake services in the test target.UIViewController+Injections
provides service access toUIViewControllers
.@dynamic
accessors makes this possible.ViewControllerTests.m
is able to use the fake service object and control the external dependency.
As I mentioned above, this example feels like overkill for this particular use case due to the simplicity of the app. In practice however this technique makes it possible to have fine grain control over a host of complicated services and objects. When your app has hundreds of classes and thousands of tests you’ll see your hard work paid back many times over. The tests will run fast and predictably (our current test suite takes under 15 seconds to run ~1400 tests). TDD and near complete test coverage can only happen when you have confidence in the test suite and you can execute them fast enough to do it often. Breaking dependencies on asynchronous code and hard to control state is crucial.
Approach this in a different way? Completely disagree? Love the technique? Reach out and let me know on twitter @sdougherty.