patternswiftModerate
Mocking UserDefaults in Swift
Viewed 0 times
swiftmockinguserdefaults
Problem
In my application, I am using
I also started writing tests for testing my application code.
At one place I stuck where I wanted to write tests for functions which are saving data to
When I wrote tests with fake data, my actual data got overwritten by fake data.
So I choose to use Mocking for
This is controller class which manages saving and fetching data from UserDefaults:
Here is
Whenever I want to interact with UserDefaults, I do it th’r above class as, e.g.:
It is working fine, no is
UserDefaults to save some data.I also started writing tests for testing my application code.
At one place I stuck where I wanted to write tests for functions which are saving data to
UserDefaults.When I wrote tests with fake data, my actual data got overwritten by fake data.
So I choose to use Mocking for
UserDefaults as follow:This is controller class which manages saving and fetching data from UserDefaults:
UserDefaultsController-
protocol UserDefaultsProtocol: class {
func theObject(forKey key: String) -> Any?
func setTheObject(_ object: Any, forKey key: String)
func removeTheObject(forKey key: String)
func synchronizeAll()
}
class UserDefaultsController: NSObject {
static let shared = UserDefaultsController()
var delegate: UserDefaultsProtocol?
override init() {
super.init()
//default delegate
delegate = UserDefaults.standard
}
func object(forKey key: String) -> Any? {
return delegate?.theObject(forKey: key)
}
func set(_ value: Any, forKey key: String) {
delegate?.setTheObject(value, forKey: key)
}
func removeObject(forKey key: String) {
delegate?.removeTheObject(forKey: key)
}
func synchronize() {
delegate?.synchronizeAll()
}
}Here is
UserDefault class extension which conforms protocol as:extension UserDefaults: UserDefaultsProtocol {
func theObject(forKey key: String) -> Any? {
return self.object(forKey: key)
}
func setTheObject(_ object: Any, forKey key: String) {
self.set(object, forKey: key)
}
func removeTheObject(forKey key: String) {
self.removeObject(forKey: key)
}
func synchronizeAll() {
self.synchronize()
}
}Whenever I want to interact with UserDefaults, I do it th’r above class as, e.g.:
UserDefaultsController.shared.set("test.com", forKey: “DomainNameKey”)It is working fine, no is
Solution
So first, as CAD97 rightly points out, if you simply define your protocol as identically matching the methods from
And there ya go, done. Having to write less code is nice. What's even nicer here is that we're doing all of this presumably for the sake of testing. If you're testing right, you're probably looking at your code coverage numbers. Using this approach, we don't have rogue uncovered lines in the extension that we're not going to hit because we're using the mock.
I'm not a huge fan of the name you've picked for the protocol. Maybe it's not so bad... and it does largely depend on what you're actually using user defaults for. But more generally, user defaults is just a key-value store for persisting information between sessions. There are other key-value store options as well though. And I believe you'd be better served by making your protocol more generally match more of the options. The advantage gained here is that swapping from user defaults to key chain to any other system because pretty effortless.
I do not like the
There's a better way.
Your current approach looks like this:
However, we could alternatively do something more like this...
Both of these are still called the same:
Because in the second implementation, we pass
And alternative, the storage could be pass in to an object's constructor:
In the main target, we don't pass anything for this argument and just let it default to the standard user defaults. But again in tests, we implement a mock class that conforms to the protocol and initialize our class with that mock.
This is called dependency injection. Instead of relying on a god class living somewhere in our code base (and increasing the coupling of our whole app), we pass dependencies into the code that relies on these dependencies. We use a protocol to keep our code base tightly aligned, but passing objects in, we keep coupling relatively low. If one class suddenly starts needing something a little different, we can just start passing it something different instead of having to touch the code in the god class... which every other corner of our code base touches... potentially introducing tons of regressions.
UserDefaults you intend to use, then making UserDefaults conform to the protocol is much simpler:extension UserDefaults: UserDefaultsProtocol {}And there ya go, done. Having to write less code is nice. What's even nicer here is that we're doing all of this presumably for the sake of testing. If you're testing right, you're probably looking at your code coverage numbers. Using this approach, we don't have rogue uncovered lines in the extension that we're not going to hit because we're using the mock.
I'm not a huge fan of the name you've picked for the protocol. Maybe it's not so bad... and it does largely depend on what you're actually using user defaults for. But more generally, user defaults is just a key-value store for persisting information between sessions. There are other key-value store options as well though. And I believe you'd be better served by making your protocol more generally match more of the options. The advantage gained here is that swapping from user defaults to key chain to any other system because pretty effortless.
I do not like the
UserDefaultsController at all. You've added an extra, unnecessary layer of complications here. It's a singleton god class... and it means that if you set up your whole app today to use user defaults, the option to change just parts of it over to a different option aren't as easy.There's a better way.
Your current approach looks like this:
func fooBarFunc() -> String? {
return UserDefaultsController.shared.object(forKey: "fooBarKey") as? String
}However, we could alternatively do something more like this...
func fooBarFunc(from storage: UserDefaultsProtocol = UserDefaults.standard) -> String? {
return storage.object(forKey: "fooBarKey") as? String
}Both of these are still called the same:
let value = fooBarFunc()Because in the second implementation, we pass
UserDefaults.standard as the default argument for the storage to pull from. However, when we want to test this method, we create a different object and pass that in:let mockStorage = MockStorage(withValue: "expectedValue")
let value = fooBarFunc(from: mockStorage)
XCTAssertEqual(value, "expectedValue")And alternative, the storage could be pass in to an object's constructor:
init(storage: UserDefaultsProtocol = UserDefaults.standard)In the main target, we don't pass anything for this argument and just let it default to the standard user defaults. But again in tests, we implement a mock class that conforms to the protocol and initialize our class with that mock.
This is called dependency injection. Instead of relying on a god class living somewhere in our code base (and increasing the coupling of our whole app), we pass dependencies into the code that relies on these dependencies. We use a protocol to keep our code base tightly aligned, but passing objects in, we keep coupling relatively low. If one class suddenly starts needing something a little different, we can just start passing it something different instead of having to touch the code in the god class... which every other corner of our code base touches... potentially introducing tons of regressions.
Code Snippets
extension UserDefaults: UserDefaultsProtocol {}func fooBarFunc() -> String? {
return UserDefaultsController.shared.object(forKey: "fooBarKey") as? String
}func fooBarFunc(from storage: UserDefaultsProtocol = UserDefaults.standard) -> String? {
return storage.object(forKey: "fooBarKey") as? String
}let value = fooBarFunc()let mockStorage = MockStorage(withValue: "expectedValue")
let value = fooBarFunc(from: mockStorage)
XCTAssertEqual(value, "expectedValue")Context
StackExchange Code Review Q#153124, answer score: 13
Revisions (0)
No revisions yet.