Resetting iOS Simulator for UI tests

Updated: Dec 30, 2019. Original version: Apr 12, 2017

One of the major issues we encounter with running UI tests for an iOS project, is the simulator’s inability to completely clear its slate between runs.

The problem we’re trying to solve

Say, you’re implementing a login functionality and, like a good citizen, you’re storing the credentials in the keychain. Now, for unit and integration tests this won’t pose a problem, as it can be easily mocked. But for UI tests, the app is tested as an self-enclosed entity, from outside, without access to internals. And the simulator doesn’t clear the keychain between tests.

Almost as bad as Xcode running tests in alphabetical order! 😱

The Solution — app launch arguments

When starting an app as part of the UI tests, one can pass in launch arguments:

func testExample() {
  let app = XCUIApplication()
  app.launchArguments = ["--Reset"]
  app.launch()
}

To understand how to use these launch arguments, we’re going to have a look at how UIApplicationMain works first.

UIApplicationMain

If you've come to Swift from other programming languages, or even if you did iOS development with Objective-C before, you might have noticed that there is no main.swift file that is used as the entry point — like the main.m in Objective-C. There is however a @UIApplicationMain attribute in the AppDelegate.swift file, that serves the same purpose. Moreover, it’s actually possible to also use a "main" entry file.

To be able to observe the new launch parameter, " — Reset", we’ll need to change things a bit so we can react to it.

Let’s delete @UIApplicationMain and create a main.swift file, besides AppDelegate.swift, with the following content:

import Foundation
import UIKit

_ = autoreleasepool {
  _ = UIApplicationMain( CommandLine.argc,
      UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to:
          UnsafeMutablePointer?.self, capacity: Int(CommandLine.argc)),
      nil, NSStringFromClass(AppDelegate.self)
  )
}

This simply calls UIApplicationMain which is the entry point to create the application object and the application delegate and set up the event cycle.

This now gives us the opportunity to perform tasks before launching the app; eg: calling a method to reset the keychain:

_ = autoreleasepool {
  if ProcessInfo().arguments.contains("--Reset") {
    AppReset.resetKeychain()
  }
  _ = UIApplicationMain( CommandLine.argc,
      UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to:
          UnsafeMutablePointer?.self, capacity: Int(CommandLine.argc)),
      nil, NSStringFromClass(AppDelegate.self)
  )
}

The AppReset

To reset the keychain I’ve created a simple resetKeychain() method, enclosed into an enum, simply for namespacing:

enum AppReset {
  static func resetKeychain() {
      let secClasses = [
          kSecClassGenericPassword as String,
          kSecClassInternetPassword as String,
          kSecClassCertificate as String,
          kSecClassKey as String,
          kSecClassIdentity as String
      ]
      for secClass in secClasses {
          let query = [kSecClass as String: secClass]
          SecItemDelete(query as CFDictionary)
      }
  }
}

All together now

Simulator reset

Here is all the code in main.swift :

import Foundation
import UIKit

enum AppReset {
  static func resetKeychain() {
      let secClasses = [
          kSecClassGenericPassword as String,
          kSecClassInternetPassword as String,
          kSecClassCertificate as String,
          kSecClassKey as String,
          kSecClassIdentity as String
      ]
      for secClass in secClasses {
          let query = [kSecClass as String: secClass]
          SecItemDelete(query as CFDictionary)
      }
  }
}

_ = autoreleasepool {
  if ProcessInfo().arguments.contains("--Reset") {
      AppReset.resetKeychain()
  }
  _ = UIApplicationMain(
      CommandLine.argc,
      UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to:
          UnsafeMutablePointer?.self, capacity: Int(CommandLine.argc)),
      nil,
      NSStringFromClass(AppDelegate.self)
  )
}

One more thing

When using a reset mechanism for UI tests, I tend to create helper methods to launch the app with different arguments:

class SomeUITests: XCTestCase {
      
  override func setUp() {
      super.setUp()
      continueAfterFailure = false
  }
  
  func launch() {
      XCUIApplication().launch()
  }
  
  func launchWithReset() {
      let app = XCUIApplication()
      app.launchArguments = ["--Reset"]
      app.launch()
  }
  
  func testExample() {
      launchWithReset()
  }
  
}

That’s it!

Happy Testing! 🤓

Web Analytics