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 a 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
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!
Top comments (0)