SwiftyUserDefaults
SwiftyUserDefaults copied to clipboard
Consider making DefaultsAdapter a class
The current design of DefaultsAdapter relies on it being used via the global variable Defaults which is a var. But using that global variable is not ideal for testing so I'm trying to use this library with typical DI.
Once doing that and storing a DefaultsAdapter as a property to be used things don't work as smoothly as expected. The problem is that the adapter is a struct and thus you can't mutate it easily. And in reality is not mutating any values so it's kind of misleading.
Changing it to a class shouldn't have any impact and would allow dynamic member lookup to be used in more places.
Setters can be declared as nonmutating, which will remove the need to declare DefaultsAdapter as a var in order to mutate the values.
Hey @alexito4 - I just merged two PRs, one that adds nonmutating keywords to the adapter's setters, and the other one for providing a protocol that you can implement to pass around testing adapters in your test suite. I'll be releasing 5.2.0 that should have both of these shortly. Please let me know if these help :)
Oh that sounds cool! I would made a task to check out the update. Thanks 😉
Thank you for this @sunshinejr!
In case anyone is interested, it took me a while but I figured out how to use SwiftyUserDefaults in the app and mock it in my tests. I'm using TCA which gives me an environment object where I pass in a UserDefaults object.
Then, I have this convenience extension method that I call on my passed UserDefaults object in the app:
import Foundation
import SwiftyUserDefaults
extension UserDefaults {
/// Returns a SwiftyUserDefaults enhanced object that can be used like the `Defaults` object in the SwiftyUserDefaults documentation.
var swifty: DefaultsAdapter<DefaultsKeys> {
DefaultsAdapter<DefaultsKeys>(defaults: self, keyStore: .init())
}
}
Then, instead of Defaults.isFirstAppStart I can now use environment.userDefaults.swifty.isFirstAppStart everywhere in the app. I've setup a custom lint rule via AnyLint to ensure no developer uses Defaults. directly.
In my test target, I have another extension for convenience:
import Foundation
extension UserDefaults {
static var test = UserDefaults(suiteName: "com.my.app.tests")!
}
This allows me to pass in UserDefaults.test to my environment object in the test suite (in the app I pass UserDefaults.standard instead). Additionally I call UserDefaults.test.removeAll() in the setUp() method of all my test classes. When I need to set a specific environment, I just set the values I need via UserDefaults.test.swifty.isFirstAppStart = false (etc.).
class LoginTests: XCTestCase {
override func setUp() {
UserDefaults.test.removeAll()
}
func testForgotPassword() {
let store = TestStore(initialState: .init(), reducer: loginReducer, environment: .init(userDefaults: .test))
UserDefaults.test.swifty.isFirstAppStart = true
// my tests expecting `isFirstAppStart` to be `true`
}
// ...
}
I hope this helps someone out there!
@Jeehut: I would recommend you use removePersistentDomain as well. As is, I believe some tests might impact others eventually.
https://www.swiftbysundell.com/tips/avoiding-mocking-userdefaults/