The biggest benefit of test-driven development (TDD) 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 folks use their regular iOS 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:
- Launch the simulator
- In the simulator, launch the app
- Inject the test bundle into the running app
- 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 iOS 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 your UIApplicationDelegate subclass to invoke 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.
Providing TestingAppDelegate
What if we can use a different application delegate for tests? What would it look like? Let’s go ahead and create it. Make sure it goes into your test target, not your app target.
Note: My instructions below cover projects that don’t use Scene Delegates. For projects that do, see How to Unit Test Scene Delegates when Swift Won’t Let You. Another good article worth checking out is How to Switch Your iOS App and Scene Delegates for Improved Testing by Geoff Hackworth.
Objective-C
TestingAppDelegate.swift (link to Gist):
import UIKit
@objc(TestingAppDelegate)
class TestingAppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
}
Add or Change main
Now let’s change the main entry point, which currently uses AppDelegate every time, explicitly or implicitly. Let’s have it do a dynamic lookup for TestingAppDelegate (which only exists in test code). If it’s there, let’s use that. Otherwise, let’s use the regular AppDelegate.
Objective-C
First, remove the @main annotation from your AppDelegate.swift.
Then create a new file main.swift (link to Gist):
import UIKit
let appDelegateClass: AnyClass =
NSClassFromString("TestingAppDelegate") ?? AppDelegate.self
UIApplicationMain(
CommandLine.argc,
CommandLine.unsafeArgv,
nil,
NSStringFromClass(appDelegateClass)
)
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.
We've reduced the "launch the app" step of testing to a bare minimum.
Even if you‘re starting a brand-new project, I recommend taking this step early because your real iOS 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 in the comments below.
I describe this technique more fully in chapter 4 of my book, iOS Unit Testing by Example: XCTest Tips and Techniques Using Swift.
Nice! I was using a more complex mechanism to identify if the app is running tests:
Also, I’m not doing the logic on main but instead on didFinishLaunchingDirectly so I have a if to check if it’s not running tests and only in that case I run all the necessary bootstrap.
One advantage of keeping the short-circuit outside the app delegate: you can write unit tests against your app delegate.
Great article, Jon. Thanks.
I have still been using a combination of both Application and Library tests for testing applications, but I’m going to switch over to this approach. Thanks for sharing it with us.
Nice of you to talk about this topic, is super important.
I’ve been using this works with old and new Xcodes, and works with Travis CI
You put it in your AppDelegate and boom! Works!
One downside is that it doesn’t work with storyboards, weird.
https://github.com/hyperoslo/NSObject-HYPTesting
I used to have calls to some kind of +isUnitTesting method, sprinkled here and there. Sometimes this is an okay starting point with legacy code, but it quickly created its own pain. I’d rather move to a single switch, moved as far outside as I can.
IMO, the easiest and more correct-ish way is to create separated target for testing.
This approach has at least two advantages:
– the ‘main’ target is clean and know nothing about tests
– you don’t have to compile parts that you’re not going to test (e.g. some view controllers, views, whatnot)
So do you put the code you want to test into two targets: your app, and your test target (where your test target doesn’t link against your app)?
Yes, some parts included in both targets. Also, this approach shows which parts could be completely decoupled from the codebase into ‘submodules’.
That’s an interesting approach Alex.
How do you feel about having to add the classes under test, and their dependencies into the test target as well? I personally don’t like having to tick those checkboxes every time.
Also why do you think your approach highlights components that can be decoupled?
I was going to say the same thing. I have separate targets for the app and for testing. That Way I just have to include the AppDelegate with nothing on it on my test target and the regular one on the main target.
It’s also easier to have dependencies that are needed just for testing on my Podfile this way.
This is great John – Thanks.
We’re using swift here so I’ve had quick go at implementing the same idea:
1: Comment out @UIApplicationMain in AppDelegate.swift
2: Make your empty TestingAppDelegate.swift
3: Make a new file called ‘main.swift’ and add the following code:
Thanks! There was a discussion on Twitter about “How do I do that in Swift?” so it’s good to have an example.
Thanks Paul Booth, you’ve put me on right track with ‘main.swift’ tip.
Some things have changed in Xcode 7/Swift 2. Following is what I’ve ended up with. Turns out you can just pass
nil
and skip creating emptyTestDelegate
.Nice shot. we are actually making return YES early in the didFinishLaunching.
Does the TestingAppDelegate needs to be compiled into the app ?
I used to use an early return in didFinishLaunching. But I found that by moving the test outside to main, it simplified the app delegate — and many more people work in the app delegate, while nobody really touches main. So it’s harder for people to accidentally break. But also, this makes it possible to write unit tests against your app delegate!
Yes, TestingAppDelegate does need to be compiled into the app. That’s a little smelly, but avoiding the need to link makes the code more complicated. I’d rather go with simple code.
Hey Jon, thanks for sharing this. I’ve been using early returns so far too, but I prefer this approach. We can look at it from the SRP point of view, and the AppDelegate should be responsible to find out whether it’s running for testing or not. I’ll convert all my current tests suites now :)
Hi Jon, I didn’t get your reply by email (I wish I had).
To no more have to have “two” delegate compiled :
So I prefer this one, as the XYZTestingAppDelegate is no part of the test bundle.
@Gio : I think the SRP is more respected this way : one AppDelegate for App, without any relationship/test for returning earlier because of tests.
Just seen the “Notify me of followup comments via e-mail” tickbox which is just after the post coment button :-)
Oh, this is better! I’ve updated my post to use this… thanks!
Couple things to note from doing this myself:
– If you’re using storyboards, you’ll need to make sure that your storyboard is not set to load automatically when the app starts up.
– If you’re using KIF tests or other UI tests, make sure that you’re checking for the existence of those classes in main before you swap out the app delegate. I generally keep my UI tests in a separate bundle from my non-UI tests, so I was able to keep the startup check in main.
If you want to take a look at how this is set up, I got it working in the TestingPlayground app I put together for my most recent talk:
https://github.com/designatednerd/TestingPlayground
Awesome stuff! Love your blog.
Why not to use for example UIResponder class instead of adding TestingAppDelegate?
Because UIResponder will always be present. But see this comment for a better way.
UIResponder could be like a stub for AppDelegate.
If we do not use our AppDelegate like a singleton in a production code, tests won’t call it and this means that we are able to stub delegate with an instance of any class (I’m using UIResponder).
And there are no need to create fake AppDelegate in production or test target.
Fake AppDelegate is a must only if our tested production code doing something like this:
This code work form me (little adjusments) in main.swift:
This technique fails for me in Xcode 7 beta, it is no longer finding the TestAppDelegate. Any pointers how to solve this?
If your TestAppDelegate is a member of your unit test bundle target, remove it and add it to your main target.
Compare what you have against my MarvelBrowser project, which works in Xcode 7: https://github.com/jonreid/MarvelBrowser
Thanks Jon,
When I try your technique on my code, with my test app delegate in my unit test target, it fails to load my test app delegate (NSClassFromString returns nil when called with my test app delegate name).
I cloned your MarvelBrowser project to try to test it there as well, but I couldn’t build it due to a missing file: “MarvelKeys.m”.
A follow-up: I commented out the code preventing the build of MarvelBrowser, and when running I experience the same result. TestingAppDelegate is never loaded. AppDelegate is used regardless if I’m running the app or the unit tests.
You’re quite correct. I suspect that the test bundle is being injected too late. I filed a Radar.
Hi Jon, what is the radar number, is it posted on openradar? I’d like to dupe it please!
I just copied it to openradar. It’s 22101460
Hi Jon, Apple replied to me that my radar is duplicating yours, and that it is closed.
Can you update what they did please?
Thanks
Basically, they said “Sorry, that’s depending on an implementation detail that can change, so this won’t work anymore.”
As recently observed, testing for the injected delegate does not work as of Xcode 7 beta 6.
However, I’ve addressed the problem similarly as the first commenter, by simply testing for the presence of an environment variable. Furthermore, I’m not even providing a substitute delegate; just passing nil as the last parameter to UIApplicationMain works fine.
Oops, I forgot to close a tag properly up there. Oh well.
I fixed it :)
@Ben solution does not work with Xcode 6.4 :-(
I suspect this also has something to do with the strange memory errors I’m seeing when running my existing unit tests that were fine before Xcode 7, in terms of the test bundle being injected “too late”.
There’s always the possibility of creating a blank class at runtime:
In my case under Xcode 7 I had to add the AppDelegateForTesting to the project target since NSClassFromString always returned nil when AppDelegateForTesting was added to the test target.
Here is the code that worked for me:
This is exactly the approach I used to take before learning about testing for classes injected via the test bundle. Now that injection is delayed on Xcode 7, we need to go back to the technique you show.
Hi there. This is not working for me. I get crashes because ‘Default context is nil! Did you forget to initialize the Core Data Stack?’ is now missing from the AppDelegate
Core data initialization has nothing to do in your app delegate (put this in another object, more respectful to the SRP).
I agree with Kenji that Core Data initialization should be handled elsewhere. And for unit tests, you’ll probably want an in-memory stack that you configure for the tests.
But that aside… the basic idea of having a TestingAppDelegate is that you can customize it if necessary, mimicking any “must-haves” from your real app delegate.
Found the solution (thanks @Simon Lucas to give me the idea to look into env vars) :
I think this solution is better than relying on runtime object creation.
Arg… work only in simulator.
On a device :
TestBundleLocation is location of the xctest bundle on the Mac side, not device one.
So I fixed it as the bundle is in the tmp directory of the app :
Tested on two devices (8.4 and 9.1)
@jon, can you edit code above to put #if !TARGET_IPHONE_SIMULATOR block inside the if( testBundlePath ) one ?
I run unit tests only on simulator.
I just updated the post for what I use now in Xcode 7.
If you want to omit the default view controller too, you can do that by providing it in the fake app delegate
Yes, once we substitute the app delegate, we have lots of control. Thanks for the example!
Love this approach, but do you know of any technique for detecting if we are running UI tests? As in this scenario we would want to load the original app delegate (the one that actually has the UI to test).
The only way I know of is less automatic. Make sure your UI tests scheme sets up an environment variable. Then in main, test for this variable with getenv.
This approach also has the added benefit of fixing some holes in the default way that Xcode calculates code coverage. I wrote about on my blog: http://cleanswifter.com/broken-code-coverage-xcode-fix/
Nice blog post Andy, thanks for sharing! You’re absolutely right, the coverage stats would be skewed by the normal app launch sequence — something that doesn’t reflect the test content in any way. We reached the same conclusion, for different purposes.
Hello, so what is the current working and preferred approach for testing AppDelegate? I’ve read that the approach described in the beginning in this blog does not work anymore. Or does it still work?
I’m currently trying to set up my testing environment based on Kiwi. I quickly learned that accessing AppDelegate directly in tests via [[UIApplication sharedApplication] delegate] does not give me enough control. So now I’m looking for any kind of ways to isloate AppDelegate for testing.
It does work. The current contents of the post are accurate at this time.
I think theres a mistake in your post. In the section “Provide TestingAppDelegate” you just have the main.swift code and not a TestAppDelegate.swift file.
Oh I see, I mistakenly repeated the main.swift part. Thanks. I also updated main.swift for Xcode 8 beta, which currently complains about Process.unsafeArgv unless it has those crazy casts.
As part of the mass Swift renaming, etc. in Swift 3 Xcode Beta 6, Process.argc and Process.unsafeArgv have been renamed to CommandLine.argc and CommandLine.unsafeArgv (link), and the UnsafeMutablePointer ‘init’ has been replaced ‘withMemoryRebound(to:capacity:_)’ (link). Still trying to understand what that means for the code in main.swift, but know it won’t work until fixed.
This seems to work in main.swift for Xcode 8 beta 6.
Nice idea, I do this:
In case anyone is interested in Mac development, the principle applies, but the implementation details are a little different. I wrote up a short blog post with the specifics.
Gotta help those Mac developers, too. Thanks, Jay!
Hi Jon,
This solution is pretty cool and easy to follow. I tried it in my sample app and it runs perfectly fine.
The same solution doesn’t work on other projects and it’s a large codebase. Multiple targets and schemes. I run a test and found that AppDelegate found nil in XCTest
Alizain, if you’re not getting an AppDelegate, it’s not the test code that will show us why. What is the structure of your app? Do you have a scene delegate?