How to Switch Your iOS App and Scene Delegates for Improved Testing

Introduction

By default, when Xcode runs unit tests it uses your main app target as a host application and effectively injects your test code into your app at run time. This makes it easy for your unit tests to refer to types and methods that are defined in your app. A side effect of this process is that every time you run your unit tests, the host application will execute its usual startup sequence, invoking the various app delegate methods and building the initial user interface (either in code or creating it from a storyboard).

Using a Test-Specific App Delegate

A few years ago I came across an article by Jon Reid titled “How to Switch Your iOS App Delegate for Improved Testing.” Jon describes a way to avoid executing the app’s usual startup/initialisation code when running unit tests. A conditional check is made very early in the lifecycle of the app (in main.swift) and uses a test-specific app delegate when the app is being launched to run unit tests. It is a simple and effective way to bypass the usual initialisation process for your app when running unit tests and does not require any changes to your main app delegate.

Sample Project

The simplest way to demonstrate the technique is to apply it to a sample iOS 13 project. I will describe the steps needed to add test-specific app and scene delegates to Xcode’s Single View Application template. You should then be able to apply the same technique to your own projects.

Create a Single View Application

To make use of iOS 13’s scene-based app lifecycle we need to be using at least Xcode 11. At the time of writing, the current released version of Xcode is 11.3.

Add a Label to the App

If you run the app with ⌘R you will see an empty white screen. If you run unit tests with ⌘U you will see Xcode briefly launch your app (and show its white screen) before returning to the home screen. The Xcode template created two empty tests in TestingSceneDelegateTests.swift. We don’t care about the actual tests for the purposes of this article; we only want to change how tests are run.

Add Test-Specific Delegates

This is where the real works happens and a number of code changes have to be made. We first need to apply Jon’s technique to create a test-specific app delegate. His article was written in a pre-iOS 13 world and there are some small differences to make it work with scenes. This is because the scene delegate, and not the app delegate, is responsible for owning the window in an app that uses the new scene-based lifecycle. We will also need a test-specific scene delegate.

Add a Test-Specific App Delegate

We need to cause the app to use a test-specific app delegate when it is being launched to run unit tests. That happens in main.swift. Swift apps don’t usually have a main.swift file, so we need to add one.

Add a Test-Specific Scene Delegate

Add a TestingSceneDelegate.swift file to the unit test target:

Run the Unit Tests

Run the unit tests again using ⌘U and you should briefly see the “Running Unit Tests…” text on a black background as the tests are run:

The Scene Configuration Caching Problem

The app delegate’s _:configurationForConnecting:options: method is called when creating new scenes. As far as I can tell, that happens when:

  • a multi-window capable app on iPadOS is asked to open a new window
  • a non-multi-window capable app is launched again after swiping to remove its window from the App Switcher

A Workaround to the Scene Configuration Caching Problem

We need to find a way to force iOS to always use TestingSceneDelegate when running unit tests without having to re-install the app (or, for non-multi-window capable apps, removing it from the App Switcher).

Support for pre-iOS 13

An app created with Xcode-11 uses the scene-based lifecycle by default. It won’t build if you change the project settings to support earlier iOS versions. There’s a great summary of the changes that are required to make an app created with Xcode 11 work with both the app- and scene-based lifecycles on Geek And Dad’s Blog: Making iOS 12 projects in Xcode 11.

Project Settings

When Xcode 11.3 created the project, it assigned iOS 13.2 as the iOS Deployment Target. We need to change this to an earlier iOS version. I arbitrarily chose iOS 11 in my sample project. The app target implicitly inherits the setting from the project and so we can change its value in the Info tab for the project:

AppDelegate

To work prior to iOS 13, AppDelegate needs to have an optional window property and the UISceneSession lifecycle methods need to be marked as only being available on iOS 13:

TestingAppDelegate

TestingAppDelegate also needs an optional window and its UISceneSession lifecycle method marked as only being available on iOS 13. The workaround code to flush the scene configuration cache needs to be enclosed in an if #available(iOS 13, *) check. Since we’re not using a storyboard when testing, prior to iOS 13 TestingAppDelegate needs to create its window programatically. Note that we need to use the default UIWindow initialiser prior to iOS 13.

SceneDelegate and TestingSceneDelegate

Since they are only used on iOS 13 we need to mark SceneDelegate and TestingSceneDelegate as only being available on iOS 13:

Conclusion

Using test-specific app and scene delegates allows our unit tests to skip executing any initialisation code and user interface configuration that the main app target performs on launch. That makes our unit tests run more quickly, which can only be a good thing!

Other Articles That You Might Like

I have written a comprehensive article on the View Controller Presentation Changes in iOS 13.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Geoff Hackworth

Independent and freelance software developer for iPhone, iPad and Mac