In my application, I am using 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 issue at all!
Now, when I write tests, I use mock object to conform to UserDefaultsProtocol as:
class UserDefaultsMock: NSObject, UserDefaultsProtocol {
private var dict = [String: Any?]()
deinit {
dict.removeAll()
}
//MARK: - Protocol Methods -
func theObject(forKey key: String) -> Any? {
if let object = dict[key] {
return object
}
return nil
}
func setTheObject(_ object: Any, forKey key: String) {
dict[key] = object
}
func removeTheObject(forKey key: String) {
dict.removeValue(forKey: key)
}
func synchronizeAll() {
//none
}
}
And here is actual test:
class ArrayTests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
UserDefaultsController.shared.delegate = UserDefaultsMock()
UserDefaultsController.shared.set("test.com", forKey: “DomainNameKey”)
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
UserDefaultsController.shared.delegate = nil
super.tearDown()
}
func testFunction() {
XCTAssertEqual(someFunctionWhichInteractsWithUserDefaults())
}
}
Now my test is not overwriting anything on my actual data, works perfect!
But my worry is, I need to write all the UserDefault APIs like string(forKey), int(forKey), etc in protocol, controller class, and in UserDefault extension and mock.
Is this is the way it should be Or am I missing any other approach on testing UserDefaults?
My only worry is because every time in my app I have to use UserDefaultsController instead of UserDefaults!
UserDefaults, you can add protocol adherence with no extra code. Then program to the protocol and inject the live or the fake object. \$\endgroup\$