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.
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?
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.
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
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
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
1 2 3 4 5 6 7 8 9 10 11
[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.
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.
FakeReachabilityManager take care of the rest.
FakeReachabilityManager provides access to toggling the reported connection status as online or offline.
1 2 3 4 5 6 7 8
-setOnline we’re able to override
- (AFNetworkReachabilityStatus)networkReachabilityStatus having it return the value of
self.fakeStatus set in the
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
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.
Environmenthandles the setup of service objects.
Environment+Fakesets fake services in the test target.
UIViewController+Injectionsprovides service access to
@dynamicaccessors makes this possible.
ViewControllerTests.mis 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.