I can successfully override the UserDefaults value by setting it with XCUIApplication.launchArguments. But based on this link and the behavior I've noticed, once I override the UserDefaults value all changes the app tries to make to it are ignored.
How do I set an initial testing value for a UserDefaults key but still allow my app to then change the value? How can I correctly test my app's response to UserDefaults updates and avoid flaky tests that have to run in order?
More background/detail:
In my iOS app, I am using UserDefaults to dictate how I render some things that should persist across app sessions. Example: I have a setting that tracks if users want to see time of day vs a countdown timer to an alarm (e.g. display "3:00pm" vs "in 1 hour"). Let's call this a boolean in UserDefaults with the key showClockTime
Users can change their preferences from a 'Settings' page that sets the UserDefaults value, and my other UIViewControllers check the UserDefaults value in viewWillAppear() so that I display the right thing.
Rather than add the same checks across the app for if this boolean is nil, true, or false I am checking it in SceneDelegate and if the value is nil set it to whatever default I prefer and then assume it's never nil in the rest of my app. (I know booleans default to returning false
instead of nil if you use UserDefaults.standard.bool("showClockTime")
but I have some String settings I'd like to test too so just go with it.)
I am trying to write integration tests to test:
nil
, I want to verify I set it to true
and the UI renders for the true
setting instead of the default false
it would be otherwise)showClockTime = false
) and I need to know that it starts as "on" before my test runs (so showClockTime
must be initially true
or toggling won't do what I expect).Not sure it matters, but other info about my app:
Tried to set UserDefaults key showClockTime
to nil as it will be on first-ever app launch and I can't ever change the setting later as my test runs.
I found a workaround but I'm not sure this is the best approach:
Overriding a UserDefaults key only makes that key immutable for the rest of the test, so instead override a test key and inside AppDelegate translate that override to the actual key that should be overridden.
In UI test:
// Assuming you want to override the actual "legitKeyName" in UserDefaults
extension XCUIApplication {
func resetLegitFlag() {
launchArguments += ["-isTestEnvironment", "true"]
launchArguments += ["-testlegitKeyName", "nil"]
}
}
Then in AppDelegate:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if (UserDefaults.standard.bool(forKey: "isTestEnvironment")) {
// We are in a test, check if we need to override a key
let actualKeyName = "legitKeyName"
let overrideValue = UserDefaults.standard.string(forKey: "test" + actualKeyName)
if (overrideValue != nil) {
switch overrideValue {
case "nil":
UserDefaults.standard.removeObject(forKey: actualKeyName)
break
default:
UserDefaults.standard.set(overrideValue, forKey: actualKeyName)
}
}
}
}
And you could do this for each flag you want to override but still change in your test.
Then in your test:
final class LegitKeyUITests: XCTestCase {
override func setUpWithError() throws {
let app = XCUIApplication()
app.resetLegitFlag()
app.launch()
...
}
}
It works for my case, but I don't like that testing code is inside AppDelegate. I tried to mitigate risk of this accidentally getting called in prod and changing real user settings by adding the second flag that specifies it's a test.