Overriding UserDefaults with XCUIApplication.launchArguments ignores all future updates to that key, so how do I test it?

88 views Asked by At

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:

  • If the user has never opened the app, do I set the correct default value in SceneDelegate before they see my first UIViewController? (meaning that: if the value is initially 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)
  • UserDefaults persist across test runs, so to get a non-flaky-test, how do I set the test's initial UserDefaults value but also test that my Settings page correctly updates the value. For example, I have a switch that I want to toggle "off" (so 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:

  • This is a vanilla Swift app, I'm not using any libraries

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.

1

There are 1 answers

0
Kay On BEST ANSWER

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.