How to Easily Switch Your App Delegate for Testing

Shares

October 25, 2016 Update: Now able to put TestingAppDelegate in test target, even in Swift

The biggest benefit of Test Driven Development is fast feedback. And so one of the best ways to keep your TDD effective is to make sure you get that feedback as fast as you can.

But most iOS folks use their production app delegate when testing. This is a problem.

That’s because when you run your tests, it first fires up your app — which probably does a lot. A bunch of slow stuff. And that’s a bunch of slow stuff we don’t need or want for our tests.

How can we avoid this?

[This post is part of the series TDD Sample App: The Complete Collection …So Far]

The testing sequence

Apple used to segregate unit tests into two categories: application tests and logic tests. The distinction was important because in the bad old days, application tests could only run on device, unless you used a completely different third-party testing framework.

But this distinction diminished when Apple made it possible to run application tests on the simulator. It took a while for their documentation to catch up, but in their latest Testing with Xcode, Apple now talks about “app tests” and “library tests.” This simplifies things down to whether you’re writing an app or writing a library. And Xcode sets up a test target for you that is usually what you need.

If I’m writing an app (or a library that requires a running app), I’m always to going to run the app tests anyway, so I stopped trying to distinguish between the two types of tests. But because Xcode executes app tests within the context of a running app, the testing sequence goes like this:

  1. Launch the simulator
  2. In the simulator, launch the app
  3. Inject the test bundle into the running app
  4. Run the tests

What can we do to make this faster? We can make step 2, “launch the app,” as fast as possible.

The normal app delegate

In production code, launching an app may fire off a number of tasks. Ole Begemann’s post Revisiting the App Launch Sequence on iOS goes into more detail, but basically, UIApplicationMain() eventually calls the app delegate to have it execute application:didFinishLaunchingWithOptions:. What that does depends on your app, but it’s not unusual for it to do things like:

  • Set up Core Data.
  • Configure the root view controller.
  • Check network reachability.
  • Send a network request to some server to retrieve the latest configuration, such as what to show in the root view controller.

That’s a lot of stuff to do before we even start testing. Wouldn’t it be better to not bother, if all we want is to run our tests?

Let’s set things up that way. Here’s how.

Change main

Let’s change our main function, which looks like this in Objective-C:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

We now want it to check if we are running tests. And if so, we want it to use a different app delegate. We can do that like this:

For Objective-C: Put the TestingAppDelegate in the test target. If we can load it, we use it.

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char *argv[])
{
    @autoreleasepool {
        Class appDelegateClass = NSClassFromString(@"TestingAppDelegate");
        if (!appDelegateClass)
            appDelegateClass = [AppDelegate class];
        return UIApplicationMain(argc, argv, nil, NSStringFromClass(appDelegateClass));
    }
}

For Swift: First, remove the @UIApplicationMain annotation from AppDelegate.swift. Then create a new file main.swift:

import UIKit

let appDelegateClass: AnyClass? =
    NSClassFromString("TEST_TARGET.TestingAppDelegate") ?? AppDelegate.self
let args = UnsafeMutableRawPointer(CommandLine.unsafeArgv)
    .bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc))
UIApplicationMain(CommandLine.argc, args, nil, NSStringFromClass(appDelegateClass!))

replacing TEST_TARGET with the name of the target containing TestingAppDelegate.

Provide TestingAppDelegate

This requires creating a new TestingAppDelegate class. In Objective-C, it’s nothing more than this:

TestingAppDelegate.h

#import <UIKit/UIKit.h>

@interface TestingAppDelegate : NSObject
@property (nonatomic, strong) UIWindow *window;
@end

TestingAppDelegate.m

#import "TestingAppDelegate.h"

@implementation TestingAppDelegate
@end

(In earlier versions of iOS, I had to add more code so that the TestingAppDelegate would create a window, give that window a do-nothing root view controller, and make the window visible. It looks like that’s no longer necessary.)

For Swift, it’s simply:

TestingAppDelegate.swift

import UIKit

class TestingAppDelegate: NSObject {
    var window: UIWindow?
}

Bare bones for fast feedback

The important thing is that we’ve reduced the “launch the app” step of testing to a bare minimum. There’s still some overhead, but not much. This is an important step for fast feedback so that we can get the most from TDD.

Even if you’re starting a brand-new project, I recommend taking this step early because your real app delegate will eventually grow. Let’s nip that in the bud, and keep the feedback fast.

Another benefit is that now we can write unit tests against the production app delegate, with complete control over what parts of it are exercised, and when. It’s win-win!

Have you used this technique, or something different? You can share your thoughts by clicking here.

This post is part of a series on What Is the Most Important Benefit of TDD? Did you find it useful? Subscribe today to get regular posts on clean iOS code.

About the Author Jon Reid

Jon is a coach and consultant on iOS Clean Code (Test Driven Development, unit testing, refactoring, design). He's been practicing TDD since 2001. You can learn more about his background, or see what services he can bring to your organization.

follow me on:

Leave a Comment:

67 comments
Add Your Reply