Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Fake AppDelegate for Unit Testing in Swift

DZone's Guide to

Fake AppDelegate for Unit Testing in Swift

When doing unit testing for mobile iOS, learn how a fake AppDelegate can help ensure fast unit testing with no side effects.

· Mobile Zone
Free Resource

Download this comprehensive Mobile Testing Reference Guide to help prioritize which mobile devices and OSs to test against, brought to you in partnership with Sauce Labs.

One of the most important things of unit testing is that it shouldn’t have side effects. If you don’t pay attention, you may have unexpected behaviors every time you run your set of tests. A fake AppDelegate may solve your problems.

Overview

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.

In this article, I’ll explain why you should use it—using a simple example—and then I’ll explain how to create it. Happy reading!

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 usually main.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 the 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:

class User {
    let identifier: Int
    let name: String

    init(identifier: Int, name: String) {
        self.identifier = identifier
        self.name = name
    }
}

class UsersProvider {

    static func fetchUsers() -> [User] {
        // Fetches from API
        // Parses data and creates an array of users
    }
}


class ViewController: UIViewController {

    private var users: [User]?

    override func viewDidLoad() {
        super.viewDidLoad()

        users = UsersProvider.fetchUsers()
    }
}

Load Programmatically

On the other hand, we can load the main UI component programmatically in the AppDelegate :

@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
    }
}

When we use this approach, we often load the data in the AppDelegate and inject it inside the view controller. 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:

class User {
    let identifier: Int
    let name: String

    init(identifier: Int, name: String) {
        self.identifier = identifier
        self.name = name
    }

}

class UsersProvider {

    static func fetchUsers() -> [User] {
        // Fetches from API
        // Parses data and creates an array of users
    }
}

class ViewController: UIViewController {

    var users: [User]?

    // ...
}

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        let users = UsersProvider.fetchUsers()
        let viewController = ViewController()
        viewController.users = users
        window = UIWindow()
        window?.rootViewController = viewController
        window?.makeKeyAndVisible()

        return true
    }
}

That’s a brief explanation of what happens when an app is launched. But you may still be wondering why you should care of these things.

When you run the unit tests, your app is launched in the target device selected (Simulator/Real Device) to load the app to test. It’s the same process which 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 the main UI component 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 users as business logic. Therefore, the fetch of users is called every time we run the unit tests.

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 behaviours when you run unit tests to avoid side effects. A solution is a fake AppDelegate.

Fake AppDelegate

Getting Started

Before starting, if you are loading the main interface from Info.plist—like I showed previously—you have to remove it and load the main storyboard programmatically:

  1. Remove the storyboard from the plist:
  2. Load the storyboard programmatically:
lass 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 storyboard
 
        window = 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:

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:”

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 behaviors in the FakeAppDelegate:

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:

let args = UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc))
 

The final main.swift file will be like this:

FakeAppDelegate

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

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:

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.

Conclusion

I want to be honest with you. I haven’t started using a fake AppDelegate since my first day as iOS developer, a day I have started having side effects with my tests—because of the business logic inside my AppDelegate—and I had to find out a solution. This may mean two things: either I’m a bad developer—which is absolutely possible—or this topic is not so trivial, since the environment doesn’t help us a lot. In either case, I hope you enjoyed this article and learned something useful.

Analysts agree that a mix of emulators/simulators and real devices are necessary to optimize your mobile app testing - learn more in this white paper, brought to you in partnership with Sauce Labs.

Topics:
mobile ,mobile app development ,mobile testing ,swift ,unit testing

Published at DZone with permission of Marco Santarossa, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}