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]
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:
What can we do to make this faster? We can make step 2, “launch the app,” as fast as possible.
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:
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.
Let’s change our main function, which looks like this in Objective-C:
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.
For Swift: First, remove the @UIApplicationMain annotation from AppDelegate.swift. Then create a new file main.swift:
replacing TEST_TARGET with the name of the target containing TestingAppDelegate.
This requires creating a new TestingAppDelegate class. In Objective-C, it’s nothing more than this:
(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:
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.
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.