Setting Keychain content in XCTest UI tests

June 3, 2017

If the state of your app relies on key/value pairs that are set in the Keychain, it’s worth testing that. However, if you’re using XCTest's UI testing framework, there’s an awkward race condition that occurs if you try to set the key/value pair in the test’s setup function.

The scenario is that the initial screen of the app defaults to a login view if there is no authentication token set in the Keychain, and loads another view if a token has been cached (in other words, implementing a persistent login). The code that checks for the presence of the authentication token is triggered by (or exists in) the AppDelegate.

(This example is from a VIPER-structured app, so the AppDelegate defers the decision about what the app’s root view should be to a Router class).

A setup function in an XCUITest like this won’t work (KeychainWrapper is referring to the SwiftKeychainWrapper library, which takes care of all the heavy lifting involved in working with the Keychain)

    override func setUp() {
        super.setUp()
        
        let _ = KeychainWrapper.standard.removeAllKeys()
        continueAfterFailure = false
        
    }

This causes a race condition - the application is spun up and the tests start before the Keychain is flushed, which means that an old token can still be lurking around from a previous test run.

The solution isn’t particulary elegant, because it relies on changing production code - but it does work.

The first step is to set a launch argument for the instance of the app that is spun up by your UI test. The obvious place for this is the test class’s setup() function:

    override func setUp() {
		
        super.setUp()
        
        continueAfterFailure = false
        
        let app = XCUIApplication()
        app.launchArguments = ["isLoggedIn"]
        app.launch()

    }

The second step is to update your AppDelegate so that it checks for the presence of this launch argument, and updates itself accordingly:

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

        // Override point for customization after application launch.
        
        // ui testing setup
        if ProcessInfo.processInfo.arguments.contains("isLoggedIn") {
            let _ = KeychainWrapper.standard.set("abcdef", forKey: Constants.kKeychainToken)
        }
        
        let router = Router()
        window = UIWindow(frame: UIScreen.main.bounds)
        
        window!.rootViewController = router.navController
        window!.makeKeyAndVisible()
        
        return true
        
    }

Launch arguments can be accessed from an instance of the ProcessInfo() class - they’re Strings contained in the arguments array, so you can easily test for their presence:

if ProcessInfo.processInfo.arguments.contains("isLoggedIn") { ... }

In this example, the AppDelegate is checking for the presence of an isLoggedIn argument, and setting a dummy login token value in the Keychain if it’s present. That value is picked up by the Router class, which instantiates the view stack with the right view controllers.

Making changes to application code to support testing doesn’t feel like the right way to go, but it is a viable workaround given the limitations of the XCTest runtime.