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\$