The Fake AppDelegate works the same as our default AppDelegates do but in a different manner. Unit tests are an important part of software development and they must be fast and without side effects. Unfortunately, if you don’t pay attention, in iOS development you may risk side effects every time you run unit tests for your application. For this reason, a fake AppDelegate is a good way to enhance your tests.
Why Fake AppDelegate?
When iOS launches an app, it needs one of the following things:
@UIApplicationMain
notation: it’s applied to a class to indicate that it is the application delegate.UIApplicationMain()
function: it’s called in the main entry point—which is usuallymain.swift
—to create the application object, the application delegate, and set up the event cycle.
By default, an iOS project has a class AppDelegate
with the notation @UIApplicationMain
. It means that this class is the entry point of your app.
Then, iOS has to find the entry point for the UI. We can use two different ways to load the main UI component: either Using Main Interface
or Load Programmatically
.
Using Main Interface
By default, an iOS project has a storyboard Main.storyboard
where we have the main UIViewController
of our app. By default, iOS searches the UI entry point inside this storyboard since it’s set in the Info.plist
:
By default the initial view controller of this storyboard is ViewController
, which becomes our main UI component.
With this approach, we often load the data of our application inside this view controller to use in the whole app:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class Person { let identifier: Int let name: String init(identifier: Int, name: String) { self.identifier = identifier self.name = name } } class PersonFetcher { static func fetchPersons() -> [Person] { // Fetches from API // Parses data and creates an array of Persons } } class ViewController: UIViewController { private var persons: [Person]? override func viewDidLoad() { super.viewDidLoad() person = PersonFetcher.fetchPersons() } } |
Load Programmatically
On the other hand, we can load the main UI component programmatically in the AppDelegate
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window = UIWindow() window?.rootViewController = UIViewController() window?.makeKeyAndVisible() return true } } |
In this example, we create a new UIViewController
and assign it to the rootViewController
of the main window. In this way the new view controller will become the main UI component of our application.
When we use this approach, we often load the data in the AppDelegate
and inject it inside the view controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class Person { let identifier: Int let name: String init(identifier: Int, name: String) { self.identifier = identifier self.name = name } } class PersonFetcher { static func fetchPersons() -> [Person] { // Fetches from API // Parses data and creates an array of Persons } } class ViewController: UIViewController { var persons: [Person]? // ... } @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { let persons = PersonFetcher.fetchPersons() let viewController = ViewController() viewController.persons = persons window = UIWindow() window?.rootViewController = viewController window?.makeKeyAndVisible() return true } } |
It’s the same process that occurs when you launch the app to debug it. There are no differences. This means that, when you run the unit tests, the AppDelegate
and main UI components are loaded as usual to run the app. If you check the example used above, you can notice that the AppDelegate
and the main UI component have the fetch of persons as business logic.
At this point, you may be thinking that it’s not a big problem. It can be a problem if at the startup of your application you read/write in a database, send API requests to insert/edit entities, or compute-heavy computations which may be time-consuming, slowing down the tests. For this reason, we need a way to skip these behaviors when you run unit tests to avoid side effects. A solution is a fake AppDelegate.
Fake AppDelegate
Before starting, if you are loading the main interface from Info.plist
—like I have shown previously—you have to remove it and load the main storyboard programmatically:
- Remove the storyboard from the plist:
- Load the storyboard programmatically:
123456789101112131415class AppDelegate: UIResponder, UIApplicationDelegate {var window: UIWindow?func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {let storyboard = UIStoryboard(name: "Main", bundle: nil) // Main is the name of storyboardwindow = UIWindow()self.window?.rootViewController = storyboard.instantiateInitialViewController()self.window?.makeKeyAndVisible()return true}}
Now we are ready to start to create a fake AppDelegate
.
Create A New App Entry Point
First of all, we need a new entry point to load either the normal or the fake AppDelegate
depending on whether the app is launched by unit tests or not.
As said at the beginning of this article, the entry point of an app can be either an AppDelegate
with the notation @UIApplicationMain
or the UIApplicationMain()
function. Now, we need the later.
The first step is creating a new file main.swift, In this file, we have to check if the app is launched by unit tests:
1 2 |
let isRunningTests = NSClassFromString("XCTestCase") != nil |
Then, we have to decide which AppDelegate
class to load. For the class to use when the app is launched by unit tests, we have two choices: we can use either a FakeAppDelegate
class where we can add the test logic to run before the tests—I’ll explain it better in “Create FakeAppDelegate”:
1 2 |
let appDelegateClass = isRunningTests ? NSStringFromClass(FakeAppDelegate.self) : NSStringFromClass(AppDelegate.self) |
Or you can merely return nil
. In this way, when you run the tests you don’t load any AppDelegate
class. It would be the fastest and recommended solution if you don’t have to add behaviours in the FakeAppDelegate
:
1 2 |
let appDelegateClass = isRunningTests ? nil : NSStringFromClass(AppDelegate.self) |
If you decide to use the nil
value you can skip the section “Create FakeAppDelegate”.
Finally, we must set the arguments used to launch our application:
1 2 |
let args = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)) |
The final main.swift
file will be like this:
FakeAppDelegate
1 2 3 4 5 6 7 |
import UIKit let isRunningTests = NSClassFromString("XCTestCase") != nil let appDelegateClass = isRunningTests ? NSStringFromClass(FakeAppDelegate.self) : NSStringFromClass(AppDelegate.self) let args = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)) UIApplicationMain(CommandLine.argc, args, nil, appDelegateClass) |
Without AppDelegate
1 2 3 4 5 6 7 |
import UIKit let isRunningTests = NSClassFromString("XCTestCase") != nil let appDelegateClass = isRunningTests ? nil : NSStringFromClass(AppDelegate.self) let args = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)) UIApplicationMain(CommandLine.argc, args, nil, appDelegateClass) |
Create FakeAppDelegate
Here we are, you have just created the file main.swift
and you decided that you want a FakeAppDelegate
class. The point is, why do we want a FakeAppDelegate
?
We know that the FakeAppDelegate
is called just once and before the unit tests. It means that you have the possibility to run test logic in your FakeAppDelegate
once and before running the set of unit tests.
Let’s look at an example:
Suppose that we want to write in a file every time we run the unit tests. We can start creating a new class called FakeAppDelegate.swift
, and in its constructor, we call the method to write the log message in a file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import Foundation class FakeAppDelegate: NSObject { private let filename = "log_tests.txt" private var filepath: URL { guard let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { fatalError() } return path.appendingPathComponent(filename) } private var logMessageData: Data { let timestamp = Date().timeIntervalSince1970 let textMessage = "Test started at \(timestamp)" guard let data = "\(textMessage)\n".data(using: .utf8, allowLossyConversion: false) else { fatalError() } return data } override init() { super.init() writeTestLog() } private func writeTestLog() { if FileManager.default.fileExists(atPath: filepath.path) { appendLog() } else { writeFirstLog() } } private func appendLog() { if let fileHandle = FileHandle(forWritingAtPath: filepath.path) { fileHandle.seekToEndOfFile() fileHandle.write(logMessageData) fileHandle.closeFile() } } private func writeFirstLog() { do { try logMessageData.write(to: filepath, options: .atomicWrite) } catch { } } } |
Remember to extend NSObject
otherwise the function UIApplicationMain
in main.swift
won’t be able to instantiate the class FakeAppDelegate
.
You can find more details here.
Thank You!!