All Posts

Unit Testing Around Singletons with OCMock

Whether you agree with the use of singletons or not, you can’t completely avoid them as an iOS developer. Apple provides some critical functionality through singletons exposed in its iOS SDK, but it can be quite difficult to write unit tests around code that accesses them. In this article, I will describe a couple of techniques for managing this. First though, let’s set up an example of the sort of problem I’m talking about.

Problem

Let’s say we have an app which is set up to register itself for push notifications. We’ve already written a simple proof-of-concept view controller with a switch for the user to enable or disable these notifications. The code is written and working well, but looks terrible.

 

I mean how is the user supposed to know what that switch does?  And is it bugging anyone else that the switch isn’t even centered? Clearly, we should make this screen a bit more interesting and add some new features. Maybe a label… some more colors…oh! how about some animation?

Whatever we decide to do, we want to make sure we don’t break the existing functionality.  The switch works, but there are no unit tests to verify that it does.

Solution

While our designer figures out what features we want to add, let’s give this code some test coverage.  Let’s start by taking a look at the code we’re going to work with.  Here is the interface for the view controller.

[cc lang=”objc”]
@interface SettingsViewController : UIViewController
{
@property (nonatomic, strong) IBOutlet UISwitch *pushNotificationsSwitch;
– (IBAction)pushNotificationsSwitchWasToggled:(id)sender;
}
[/cc]

…and here’s the implementation.

[cc lang=”objc”]
@implementation SettingsViewController

– (void)viewDidLoad
{
//create the switch
self.pushNotificationsSwitch = [[UISwitch alloc] initWithFrame:CGRectMake(0, 0, 50, 30)];

//link it to the action method
[self.pushNotificationsSwitch addTarget:self action:@selector(pushNotificationsSwitchWasToggled:) forControlEvents:UIControlEventValueChanged];
}

– (IBAction)pushNotificationsSwitchWasToggled:(id)sender
{
UISwitch *toggleSwitch = sender;
if (toggleSwitch.on)
{
//if the switch was toggled on, register for push notifications
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert)];
}
else
{
//if it was toggled off, unregister
[[UIApplication sharedApplication] unregisterForRemoteNotifications];
}
}
@end
[/cc]

We’ll first try to write a test to verify that the app gets registered for push notifications when that lonely switch gets flipped to ON.

[cc lang=”objc”]
– (void)testToggleOnRegistersForPushNotifications_On
{
//given the view controller is loaded
SettingsViewController *sut = [[SettingsViewController alloc] init];
[sut view];

//given the switch is toggled on
[sut.pushNotificationsSwitch setOn:YES];

//when the view controller is notified that the switch was toggled
[sut pushNotificationsSwitchWasToggled:sut.pushNotificationsSwitch];

//then the app should register for notifications with alerts and badges
//?? how do we verify this
}
[/cc]

Let’s walk through what’s going on here.  We create an instance of the SUT, then simulate the act of flipping the switch to ON. Unfortunately, we’re not sure now how to verify that the switch did what it’s supposed to because all it does is call methods on the UIApplication singleton! To finish the test, we need to figure out a way to expose [UIApplication sharedApplication] as a dependency. One way to do that is through dependency injection.

With Dependency Injection

To use dependency injection, we’ll have to modify our view controller class so we can provide an instance of UIApplication that it can work with. Then the app can use the real sharedApplication instance, while the test uses a mock object.  To accomplish this, we’ll add a new property to the SettingsViewController interface…

[cc lang=”objc”]
@property (nonatomic, strong) UIApplication *application;
[/cc]

…and modify our view controller implementation to use it.

[cc lang=”objc”]
– (UIApplication*)application
{
if (!_application)
{
_application = [UIApplication sharedApplication];
}
return _application;
}

– (IBAction)pushNotificationsSwitchWasToggled:(id)sender
{
UISwitch *toggleSwitch = sender;
if (toggleSwitch.on)
{
[self.application registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert)];
}
else
{
[self.application unregisterForRemoteNotifications];
[/cc]

The first method here is lazy-loading the application property so that it defaults to [UIApplication sharedApplication] when the app is running for real.  Then we changed pushNotificationsSwitchWasToggled: to refer to self.application instead of [UIApplication sharedApplication].

With that out of the way, we can finish our test by initializing the view controller with a mock object before the switch is toggled. The test can then monitor that mock to verify the behavior we’re expecting. Here’s our unit test again. This time, the test is using the mocking framework OCMock to create the mock, set up expectations, and verify them.

[cc lang=”objc”]
– (void)testToggleOnRegistersForPushNotifications_On
{
//given
id mockApplication = [OCMockObject niceMockForClass:[UIApplication class]];
[[mockApplication expect] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert)];

SettingsViewController *sut = [[SettingsViewController alloc] init];
sut.application = mockApplication;
[sut view];
[sut.pushNotificationsSwitch setOn:YES];

//when
[sut pushNotificationsSwitchWasToggled:sut.pushNotificationsSwitch];

//then
[mockApplication verify];
}
[/cc]

Here our mock instance is configured to expect a message with the signature registerForRemoteNotificationTypes: and some very specific arguments. We then simulate flipping the switch to ON. At the end of the test, we ask the mock to verify that the expected method was actually called. If not, the test raises an exception on that last line and the test is reported as a failure.

You’ll notice that to use dependency injection, we had to modify a lot of our app’s code to make it testable.  Imagine SettingsViewController was a much larger and more complicated class without any unit test coverage. It could be quite intimidating to have to change that code just to make it testable. You could break it just trying to test it. If for some reason you can’t or don’t want to take that risk, there’s another way to test this code.

Without Dependency Injection

In the case of our SettingsViewController, we can actually stub out this singleton dependency without resorting to dependency injection. OCMock in particular is actually quite helpful in this case. With the help of OCMock, we can temporarily replace the singleton instance with a mock object that can be monitored by our test.

Here’s that same test again, but this time, we’ll do our mocking without dependency injection.

[cc lang=”objc”]
– (void)testToggleOnRegistersForPushNotifications_On
{
//given a mock UIApplication
id mockApplication = [OCMockObject niceMockForClass:[UIApplication class]];
[[[mockApplication stub] andReturn:mockApplication] sharedApplication];
[[mockApplication expect] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert)];

//given the view controller is loaded
SettingsViewController *sut = [[SettingsViewController alloc] init];
[sut view];

//given the switch is toggled on
[sut.pushNotificationsSwitch setOn:YES];

//when the view controller is notified that the switch was toggled
[sut pushNotificationsSwitchWasToggled:sut.pushNotificationsSwitch];

//then the app should register for notifications with alerts and badges
[mockApplication verify];

//finally clean up the mock (VERY IMPORTANT)
[mockApplication stopMocking];
}
[/cc]

Here we simply stub out the singleton accessor method and force it to return our mock object. Now, whenever any code in the app calls [UIApplication sharedApplication], it will actually get this instance of OCMockObject instead.

You may have noticed that the first time we wrote this test we didn’t call stopMocking at the end. Normally, at the end of a test, you don’t have to worry about cleaning up a mock instance like this. A normal mock just evaporates as soon as it falls out of scope. But not this one. Since we’re mocking a singleton, the instance won’t actually fall out of scope after the test.

Try this to see what I mean. Comment out the stopMocking line in the test above and add this test at the end of the same test suite:

[cc lang=”objc”]
– (void)testZ_MockWasActuallyDestroyed
{
id app = [UIApplication sharedApplication];
XCTAssert([app isMemberOfClass:[UIApplication class]],
@”a mock for UIApplication sharedApplication was not cleaned up!”);
}
[/cc]

The ‘Z’ at the front of the test name just forces it to run last. This is because OCUnit/XCUnit tests run in alphabetical order.  With the call to stopMocking commented out, you’ll see that this new test fails.

So it’s VERY IMPORTANT that you call OCMock’s stopMocking method on the mock singleton object when you’re done with it. Forgetting this step means your mock singleton instance will survive and potentially influence the outcome of other unit tests.

You can find a sample project on github which shows all the code above in action.  It also includes some alternate unit tests which demonstrate doing some of the same things using Jon Reid’s OCMockito, instead of OCMock.

[amp-cta id=’8486′]