When an app launches, it kicks off setup operations like asking the remote API for new data, loading information from the local storage, or checking-in with analytics providers.
All this work gives the user a smooth startup experience but is unnecessary when running the unit tests and dangerous too: it can meddle with the global state, resulting in hard to diagnose failures. Sometimes, it can even make the tests noticeably slower or log noise into your analytics.
One of the first things I do when taking up a new project, whether greenfield or established, is preventing the unit tests from running the app startup flow.
I published a post about this back in 2016, when UIKit was the only framework in town. It's now time to revisit it for SwiftUI based applications.
SwiftUI App
// AppLauncher.swift
import SwiftUI
@main
struct AppLauncher {
static func main() throws {
if NSClassFromString("XCTestCase") == nil {
MyAwesomeApp.main()
} else {
TestApp.main()
}
}
}
struct TestApp: App {
var body: some Scene {
WindowGroup { Text("Running Unit Tests") }
}
}
// MyAwesomeApp.swift
import SwiftUI
struct MyAwesomeApp: App {
var body: some Scene { ... }
}
You can find the source code for this example on GitHub.
Let's unpack what the code does.
First of all, the top-level entry point for the program flow, marked by the @main
attribute is AppLauncher
.
That's different from the default code generated by Xcode, where MyAwesomeApp
would have been the entry point.
This dedicated launcher allows us to differentiated between tests and genuine runs of the app.
To support the @main
, AppLauncher
must provide a static main
method that the OS will call when launching the executable.
Checkout Keith Harrison's post "What does @main
do in Swift 5.3" for a deep dive into how the app launches.
AppLauncher
controls whether we're running the unit tests using the availability of the XCTestCase
class as a proxy.
If the class is found, the XCTest framework is loaded, which means the app launched as the host of the tests target.
Finally, if AppLauncher
determines that the tests are running, it returns a dummy App
implementation.
SwiftUI with UIKit App Delegate
If you are mix-and-matching SwiftUI and UIKit, that is, if you have an app with SwiftUI interface and "UIKit App Delegate" life cycle, then you can use the same approach as a UIKit only app.
// main.swift
import UIKit
private func delegateClassName() -> String? {
if NSClassFromString("XCTestCase") == nil {
NSStringFromClass(AppDelegate.self)
} else {
return nil
}
}
UIApplicationMain(
CommandLine.argc,
CommandLine.unsafeArgv,
nil,
delegateClassName()
)
// AppDelegate.swift
import UIKit
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
}
This example is also available on GitHub.
Another option is to use an AppLauncher
returning a different UIApplicationDelegate
implementation for the tests, like in the SwiftUI only case.
I find this approach with a dedicated main.swift
file easier to discover, and I like how there is no need for a dummy app delegate when running the tests.
I hope you'll find this technique useful. If you have any questions or need help adopting this approach in your codebase, leave a comment below or get in touch on Twitter at @mokagio.