17

In most of my classes that work with defaults I make the defaults object settable:

@property(retain) NSUserDefaults *defaults;

This is supposed to make testing easier:

// In a nearby test class:
- (void) setUp {
    [super setUp];
    NSUserDefaults *isolatedDefaults = [[NSUserDefaults alloc] init];
    [someObjectBeingTested setDefaults:isolatedDefaults];
}

But now I have found out then when I create a fresh defaults object, there are already some values in it. Is that possible? I thought I could create an empty, isolated defaults object by calling -init. Do I have a bug somewhere in the testing code, or do I really have to do something more complex (like stubbing or mocking) if I want to test my defaults-based code?

zoul
  • 102,279
  • 44
  • 260
  • 354

3 Answers3

8

In the end I have created a simple NSUserDefaults replacement that can be used to control the defaults environment in tests. The code is available on GitHub.

zoul
  • 102,279
  • 44
  • 260
  • 354
  • very nice, thanks! I'm now using your class, but instead of injecting `NSUserDefaults` as a param to `someObjectBeingTested` I'm using `Kiwi` with its mocking ability to stub. Using something like: `id transientDefaults = [NSUserDefaults transientDefaults]; [NSUserDefaults stub:@selector(standardUserDefaults) andReturn:transientDefaults];` – kernix Jan 21 '15 at 20:53
6

From the NSUserDefaults documentation:

init: Returns an NSUserDefaults object initialized with the defaults for the current user account.

This should normally not be empty. I am not really sure what you want to test here, since it would be a waste of time to test NSUserDefaults functionality.

But say you need some keys to be not registered yet for your test to always have the same initial point: then just remove them in setUp (and restore them later in tearDown if you want to).

Something like:

- (void) setUp {
  [super setUp];

  [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"myTestKey"];

  // synchronize the change, or just use resetStandardUserDefaults:
  [someObjectBeingTested setDefaults:[NSUserDefaults resetStandardUserDefaults]];
}

If you don't have a specific list of keys but need to wipe out everything, you will have to use the CoreFoundation Preferences Utilities, see CFPreferencesCopyKeyList.

w-m
  • 10,772
  • 1
  • 42
  • 49
  • I want to test code that uses user defaults for some input and output. If I used “real” user defaults I would have hard time controlling the environment – I’d have to delete unwanted values, restore others after running the tests and so on. In the end I wrote a kind of defaults replacement for testing purposes, see my answer. Thank you! – zoul Nov 29 '10 at 16:28
  • For some reason setting a BOOL key to NO did not work, but using removeObjectForKey worked perfectly. So thanks for the tip. – phatmann Jan 17 '14 at 00:55
0

We also needed to override NSUserDefaults for testing, but didn't want to change any of the application code.

So we wrote a category on NSUserDefaults that allows us to override values returned by objectForKey: at runtime using method swizzling.

It looks like this in Objective C:

NSLog(@"Value before: %d", [[NSUserDefaults standardUserDefaults] boolForKey:@"Example"]);
// Value before: 0

[[NSUserDefaults standardUserDefaults] overrideValue:@(YES) forKey:@"Example"];

NSLog(@"Value after: %d", [[NSUserDefaults standardUserDefaults] boolForKey:@"Example"]);
// Value after: 1

And like this in Swift:

print(UserDefaults.standard.bool(forKey: "ExampleKey")) // false

UserDefaults.standard.overrideValue(true, forKey: "ExampleKey")

print(UserDefaults.standard.bool(forKey: "ExampleKey")) // true

You can find our code on Github: https://github.com/jakob/NSUserDefaultsOverride

Jakob Egger
  • 11,981
  • 4
  • 38
  • 48