Testing NSUserDefaults

8 March 2015

NSUserDefaults is where you can store persistent app settings, as an alternative to storing them in some kind of in-app database. You shouldn’t try to store actual application data in NSUserDefaults, but they are useful for capturing “small” settings like environment URLs and so on.

If you’re using NSUserDefaults, you should be testing NSUserDefaults. Here’s how.

One of the most common testing scenarios is that you want to store some value in the defaults store so that it can be read out later. This is the process for testing that your code actually does so. Assume that we’ve got a class called myClass which has an IBAction method called didTapUpdateSettingsButton which will write out a specific value.

{% codeblock lang:objc) it(@“should write the configured environment to the user defaults”), ^{

NSString *apiUrl = @"http://foo.com/bar";
NSString *theKey = @"defaultApiUrl";

id mockStandardDefaults = [NSUserDefaults nullMock];

[NSUserDefaults stub:@selector(standardUserDefaults) andReturn:mockStandardDefaults];

[[mockStandardDefaults should] receive:@selector(setObject:forKey:) withArguments:theObject, theKey];

[[mockStandardDefaults should] receive:@selector(sychronize)];

[myClass didTapUpdateSettingsButton:nil];

} {% endcodeblock)

Walking through this, we first create variables for the key and object that we are expecting the didTapUpdateSettings method to attempt to write to the defaults store.

Next, we create a mock object which will stand in for the instance of the NSUserDefaults class that would normally be returned by the [NSUserDefaults standardUserDefaults] method. We’re going to use this mock to catch and test the messages that our code under test sends. It’s created as a null mock here so that it won’t complain if it gets messages other than the ones that we send it.

Then we need to stub the standardUserDefaults method of the real, live NSUserDefaults class, and return our mock instance instead of the real thing.

Once we’ve stubbed out NSUserDefaults, we can now set some expectations about what our mock standardUserDefaults will receive. There are two things we’re interested in - firstly, that it receives the setObject:forKey message with the values that we expect. If that message isn’t received, or the values differ from what we expect, then there’s something wrong with the code that we’re writing.

Secondly, we’re also setting an expectation that the mock receives the synchronize message. This saves the values that we’ve updated - not sending a synchronize message to NSUserDefaults is a common mistake, and hard to diagnost - with a specific test, we’ll catch any situations where we forget that final step.

Finally, we invoke the code under test by “tapping” the button (or in this case, calling the IBAction method that the button is wired up to).

If you’re taking a test-first approach to writing the code, obviously this will initially fail. But by fixing each failing assertion in turn, you’ll end up with a method that both writes and saves the correct values.