Why Unit Testing in iOS is not like others?
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.