Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to clear NSUserDefaults programmatically in XCUITest using Simulator

I've read several answers related to this and they suggest doing one of the following, but these options are not working for me. I have an XCUITest and I'm trying to clear the standard user defaults before before running the rest of my XCUITest. Currently my test app has a button that calls this code. I've also tried calling this code directly from within the XCUITest (I'm not sure if this is expected to work or if it needs to be run from within the app).

NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain];

I've also tried removing each individually:

[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"MyKey1"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"MyKey2"];

I also tried each of the above methods followed by a synchronize call:

[[NSUserDefaults standardUserDefaults] synchronize];

The next time I read @"MyKey1" from the NSUserDefaults its still has the old value and has not been deleted.

Is there any way to remove an object from the NSUserDefaults programmatically when running an XCUITest in the simulator? These are automated tests, so I can't always manually click on "Reset Contents and Settings" in xcode.

Thanks!

like image 680
dannyp32 Avatar asked Dec 24 '22 04:12

dannyp32


1 Answers

There are couple of ways to solve your issues by setting the UserDefaults values before your tests run the app (not after).

Solution #1

Mock UserDefaults values for certain keys using launchArguments:

func testExample() {
    let app = XCUIApplication()
    app.launchArguments += ["-keepScreenOnKey", "YES"]
    app.launch()
}

Note the minus sign before the keepScreenOnKey key. The minus sign indicates that it should take the next launch argument value as a value for that UserDefaults key.

Solution #2 (if solution #1 doesn’t work)

The previous solution might not work if you’re using the SwiftyUserDefaults library. In this case, there is one elegant solution: if the application is running under UI tests, erase all UserDefaults data from the app. How does the app know if it is being run under UI tests? By passing the launch arguments message, like this:

override func setUp() {
    super.setUp()
    app.launchArguments += ["UI-Testing"]
}

Then, in AppDelegate.swift check if the app is running under UI-Testing and remove all UserDefaults data (like you do):

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        setStateForUITesting()

        return true
    }

    static var isUITestingEnabled: Bool {
        get {
            return ProcessInfo.processInfo.arguments.contains("UI-Testing")
        }
    }

    private func setStateForUITesting() {
        if AppDelegate.isUITestingEnabled {
            UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
        }
    }
}

Solution #3 (if solution #2 is not enough)

But what if the UI test expects states other than the default state set by solution #2? Bools default to false, Integers to 0 and Strings to "", but what if you need true for the Bool key?

Do something like this:

func testExample() {
    app.launchEnvironment["UI-TestingKey_keepScreenOn"] = "YES"
    app.launch()
}

And then in AppDelegate.swift file:

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    static let uiTestingKeyPrefix = "UI-TestingKey_"

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        if AppDelegate.isUITestingEnabled {
            setUserDefaults()
        }

        return true
    }

    static var isUITestingEnabled: Bool {
        get {
            return ProcessInfo.processInfo.arguments.contains("UI-Testing")
        }
    }

    private func setUserDefaults() {
        for (key, value)
            in ProcessInfo.processInfo.environment
            where key.hasPrefix(AppDelegate.uiTestingKeyPrefix) {
            // Truncate "UI-TestingKey_" part
            let userDefaultsKey = key.truncateUITestingKey()
            switch value {
            case "YES":
                UserDefaults.standard.set(true, forKey: userDefaultsKey)
            case "NO":
                UserDefaults.standard.set(false, forKey: userDefaultsKey)
            default:
                UserDefaults.standard.set(value, forKey: userDefaultsKey)
            }
        }
    }
}

extension String {
    func truncateUITestingKey() -> String {
        if let range = self.range(of: AppDelegate.uiTestingKeyPrefix) {
            let userDefaultsKey = self[range.upperBound...]
            return String(userDefaultsKey)
        }
        return self
    }
}

Please note that this example only works for Bool and String keys. If you need more scalability, the switch command should be modified to somehow check if the value is Integer or Double or Any other value, but the general idea is here.

EDIT: It looks like the reason for using solutions #2 and #3 is not valid anymore as of SwiftyUserDefaults version 4.0.0-beta.1 as they've added support for setting values through launch arguments. But, I have to admit that I have not tested SwiftyUserDefaults library from this version onward, so I'll keep both solutions here.

like image 61
Mladen Avatar answered May 16 '23 06:05

Mladen