Fake AppDelegate For Unit Testing In Swift
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 behaviours 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 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:
|
|
Load Programmatically
On the other hand, we can load the main UI component programmatically in the AppDelegate
:
|
|
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:
|
|
That’s all, you had 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 shown previously—you have to remove it and load the main storyboard programmatically:
Remove the storyboard from the plist:
Load the storyboard programmatically:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
class 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:
|
|
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”:
|
|
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
:
|
|
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:
|
|
The final main.swift
file will be like this:
FakeAppDelegate
|
|
Without AppDelegate
|
|
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:
|
|
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 cases, I hope you enjoyed this article reading something useful.