swiftnsuserdefaultsxcode-ui-testing

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


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:

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.


Solution

  • 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.