Last updated: •Original version:
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.
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! 😱
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.
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)
)
}
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)
}
}
}
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)
)
}
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()
}
}
Happy Testing! 🤓