2

I am trying to mock UserDefaults to be able to test its behaviour.

What is the best way to do it without corrupting the real computed property that save the user token key?

class UserDefaultsService {

  private struct Keys {
    static let token = "partageTokenKey"
  }

  //MARK: - Save or retrieve the user token
  static var token: String? {
    get {
      return UserDefaults.standard.string(
        forKey: Keys.token)
    }
    set {
      UserDefaults.standard.set(
        newValue, forKey: Keys.token)
    }
  }
}
Roland Lariotte
  • 2,606
  • 1
  • 16
  • 40

4 Answers4

5

You can subclass UserDefaults :

(source)

class MockUserDefaults : UserDefaults {

  convenience init() {
    self.init(suiteName: "Mock User Defaults")!
  }

  override init?(suiteName suitename: String?) {
    UserDefaults().removePersistentDomain(forName: suitename!)
    super.init(suiteName: suitename)
  }

}

And in UserDefaultsService instead of directly accessing UserDefaults.standard you can create property based on the target that you are running. In production/staging you can have UserDefaults.standard and for testing you can have MockUserDefaults

You should add PREPROCESSOR Flag before using them

#if TESTING
    let userDefaults: UserDefaults = UserDefaults.standard
    #else
    let userDefaults: UserDefaults = MockUserDefaults(suiteName: "testing") ?? UserDefaults.standard
#endif
wootage
  • 936
  • 6
  • 14
3

A very good solution is to not bother creating a mock or extracting a protocol. Instead init a UserDefaults object in your tests like this:

let userDefaults = UserDefaults(suiteName: #file)
userDefaults.removePersistentDomain(forName: #file)

Now you can go ahead and use the UserDefaults keys you already have defined in an extension and even inject this into any functions as needed! Cool. This will prevent your actual UserDefaults from being touched.

Brief article here

SmileBot
  • 19,393
  • 7
  • 65
  • 62
2

One way of doing it is to wrap your UserDefaults in a protocol and expose what you need.

Then you create a an actual class which conforms to that protocol and which uses UserDefaults

You can then instantiate your UserDefaultsService with that class.

When you need to test, you can create a mock conforming to the same protocol and use that instead. That way you won't "pollute" your UserDefaults.

The above might seem like a bit of a mouthful so lets break it down.

Note that in the above I removed the "static" part as well, it didn't seem necessary, and it made it easier without it, hope that is OK

1. Create a Protocol

This should be all you are interested in exposing

protocol SettingsContainer {
    var token: String? { get set }
}

2. Create an Actual Class

This class will be used with UserDefaults but it is "hidden" behind the protocol.

class UserDefaultsContainer {
    private struct Keys {
        static let token = "partageTokenKey"
    }
}

extension UserDefaultsContainer: SettingsContainer {
    var token: String? {
        get {
            return UserDefaults.standard.string(forKey: Keys.token)
        }
        set {
            UserDefaults.standard.set(newValue, forKey: Keys.token)
        }
    }
}

3. Instantiate UserDefaultsService With That Class

Now we create an instance of your UserDefaultsService which has an object conforming to the SettingsContainer protocol.

The beauty is that you can change the provided class later on...for instance when testing.

The UserDefaultsService does not know - or care - whatever the SettingsContainer actually does with the value, as long as it can give and take a token, then the UserDefaultsService is happy.

Here's how that looks, note that we are passing a default parameter, so we don't even have to pass a SettingsContainer unless we have to.

class UserDefaultsService {
    private var settingsContainer: SettingsContainer

    init(settingsContainer: SettingsContainer = UserDefaultsContainer()) {
        self.settingsContainer = settingsContainer
    }

    var token: String? {
        get {
            return settingsContainer.token
        }
        set {
            settingsContainer.token = newValue
        }
    }
}

You can now use a new UserDefaultsService like so:

let userDefaultsService = UserDefaultsService()
print("token: \(userDefaultsService.token)")

4 Testing

"Finally" you say :)

To test the above, you can create a MockSettingsContainer conforming to the SettingsContainer

class MockSettingsContainer: SettingsContainer {
    var token: String?
}

and pass that to a new UserDefaultsService instance in your test target.

let mockSettingsContainer = MockSettingsContainer()
let userDefaultsService = UserDefaultsService(settingsContainer: mockSettingsContainer)

And can now test that your UserDefaultsService can actually save and retrieve data without polluting UserDefaults.

Final Notes

The above might seem like a lot of work, but the important thing to understand is:

  • wrap 3rd party components (like UserDefaults) behind a protocol so you are free to change them later on if so needed (for instance when testing).
  • Have dependencies in your classes that uses these protocols instead of "real" classes, that way you - again - are free to change the classes. As long as they conform to the protocol, all is well :)

Hope that helps.

pbodsk
  • 6,787
  • 3
  • 21
  • 51
  • It does work when using UserDefaults, but when I get to testing it, it shows an error: "Argument type 'MockSettingsContainer.Type' does not conform to expected type 'SettingsContainer''. I clearly write the class as you did and conform the mock to the protocol for my test, but the error appears at that point: "let userDefaultsService = UserDefaultsService(settingsContainer: MockSettingsContainer)" – Roland Lariotte Aug 06 '19 at 12:50
  • 1
    I don't know if that is just a typo, but it seems you are using it with the class, note the capital M in `MockSettingsContainer` and no parentheses. I skimmed my answer and can see where that confusion might stem from, so I added a line initializing the `MockSettingsContainer` (under "4 Testing"). It boils down to this: `let mockSettingsContainer = MockSettingsContainer() let userDefaultsService = UserDefaultsService(settingsContainer: mockSettingsContainer)` Hope that helps you – pbodsk Aug 06 '19 at 12:57
  • 1
    @Sparklydust have you added `@testable import YourProjectName` in the testing file? – wootage Aug 06 '19 at 13:16
-1

Instead of mocking UserDefaults, you should check the value you persist in UserDefaults, should get retrieved from the same user defaults by accessing using same key. the test case should look like this.

func testWhenTokenIsSavedInUserDefaults_ReturnSameTokenAndVerify {
  let persistenceService = UserDefaultsService()
  persistenceService.token = "ABC-123"

  let defaults = UserDefaults.standard
  let expectedValue = defaults.value(forKey: "partageTokenKey")

  XCTAssertEquals(persistenceService.token, expectedValue)
}