Unit Testing

Why Unit Testing in iOS is not like others?

Mehdi Samadi
3 min readApr 27, 2022

Unit testing in iOS has a little bump at first and this discourages developers at first. In Python, for example, you write the test code just like the other code. Learn a few basics about the unittest or nose2 and you are on your way. You don’t need to run the whole application to run the test. But in iOS, you can’t run the test code without running the whole app, and that is the problem that could hold you from writing tests and using TDD.

So, the first thing you need for testing your app is to stop it!

App Launch

Any iOS App is an instance of UIApplicationMain, but the actual app runs by AppDelegate so we have to have our own AppDelegate to control the app launch.

If you search for UIApplicationMain in a standard single-page app template, you can’t find it. Except for an attribute @UIApplicationMain on top of the AppDelegate that tells the Xcode, hey this is the one you should use.

To create UIApplicationMain yourself, you must remove this attribute and create a file called main.swift and put one line of code there to create an instance of UIApplicationMain .

UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, AppDelegateClassName)

The funny part is that UIApplicationMain only need the AppDelegate ‘s name as String. It knows what to do with it, smart kid!

Now create your own TestAppDelegate , override didFinishLaunchingWithOptions and you are in control. But how you will decide which AppDelegate to load?

import UIKit
@objc(TestingAppDelegate)
class TestingAppDelegate: UIResponder, UIApplicationDelegate {

func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print("Launching with testing app delegate")
return true
}
}

Loading TestAppDelegate

In unit testing, you need to access your app’s code, but Xcode creates a new target for the test and you probably know that the code is not shared between targets. (you can share it but, I can tell you it is not fun during debugging!). When you are testing, iOS creates both targets but before running the app, it injects the test target into the app and then runs the app.

This is the main difference with UI Testing. In the UI Testing, the test and app targets run independently, and they only have a communication path through the accessibility system. Whereas in unit testing, the test target compiles and injects into the app target where both live together.

Note that because UI Testing runs the actual app target, the TestingAppDelegate will not load and TestingAppDelegate can only be injected for unit testing.

Test or Production Environment

We need a function to tell us if we are running in a test or production environment. To get the difference we use the fact that the code which is belong to the test target only compiles for testing. So if a class resides only on the test target, existing that class during runtime indicates that we are in a test environment. Thanks to NSClassFromString you can check the existence of a class in your codebase. In this case, if we define TestAppDelegate in the test target then this class will only exist during the test and we can load it automatically by chain operator.

// main.swift

import UIKit

let appDelegateClass: AnyClass = NSClassFromString("TestingAppDelegate") ?? AppDelegate.self

UIApplicationMain(
CommandLine.argc,
CommandLine.unsafeArgv,
nil,
NSStringFromClass(appDelegateClass))

Usually, for UI testing we pass some parameters as the application’s launch arguments and this could be a security issue if you let the code run in a production environment.

    func isUITesting() -> Bool {
NSClassFromString("UITestingClass") != nil
}
func isUnitTesting() -> Bool {
NSClassFromString("TestingAppDelegate") != nil
}

Define these two functions inside appDelegate and secure your launch arguments. UITestingClass is an empty class that belongs to the UI Testing target.

Summary

The whole point of Unit Testing is to test individual classes of your app. Stop launching the app, in order to run unit tests.

I hope you are now, more encouraged to write a test 🙂.

If you want to read more about unit testing in iOS, “iOS Unit Testing by Example” is a great source.

--

--