Problem using fake SceneDelegate for unit tests

I would like to use a fake SceneDelegate for unit tests so that the app does not create numerous real dependencies at launch. I used to create a fake AppDelegate and it worked great.

The problem is that application(_:configurationForConnecting:options:) is only called after first app launch.

It is unexpected that the function is not called on every launch. I read the documentation and did not find anything describing when this function is and is not called:

https://developer.apple.com/documentation/uikit/uiapplicationdelegate/3197905-application

Because this function is only called during first app launch I have a problem when I run unit tests. The intention is to use TestAppDelegate and TestSceneDelegate when running unit tests so that my tests don't execute a bunch of real app launch code and use dependencies that are not mocked. This is a pretty common and good approach it seems:

If I run unit tests AFTER I run the app the TestAppDelegate is used but because the function is not called SceneDelegate is used instead of TestSceneDelegate. It's like the system caches the UISceneConfiguration.

This is bad because unintended production code is executed when I run tests.

I can work around the problem by deleting the app before I run unit tests.

It is interesting that if I run the app after running unit tests the function does get called again and so there is no problem reversing the steps.

Really hoping there is a way to specify UISceneConfiguration for each app launch because this is the cleanest way to setup for testing.

Thanks!
Answered by Apple Designer in 613994022
Hi subclass,

I'm familiar with the pattern you're using to swap out your App Delegate for unit testing. I'm wondering if you've considered using framework-hosted tests, as opposed to app-hosted tests, so your app and scene delegates wouldn't be used at all. There's also a speed improvement to your tests if you take this approach since it won't install your app into a simulator.

Assuming you can't make your tests framework-hosted rather than app-hosted (Which you can enable by changing which host application your test target uses in its General settings), I'll admit I'm not sure if you can configure your scene delegate the way you'd like based on the caching behavior you seem to be noticing, though I have 2 ideas:

1) Make your TestAppDelegate and real AppDelegate conform to both the UIApplicationDelegate and UISceneDelegate protocol, and then use composition to forward the calls into separate implementation classes (so as to maintain the separation of concerns that's ideal with the SceneDelegate and AppDelegate classes we generate for you). I haven't tested this but it seems like, if you made both your AppDelegate and TestAppDelegate handle both protocol conformances, this would work to make sure your test double gets loaded consistently.

2) Alternatively, add a switch on the launch arguments of your iOS app inside the concrete SceneDelegate class to control the path it takes when testing, perhaps via a --unit-testing flag, so you can avoid expensive, stateful, or tricky logic you'd like to avoid for testing. You can again use composition and delegation here to keep the SceneDelegate code manageable. If you're concerned about compiling in test hooks into your app code, you can also conditionally compile this code out for release builds.




Here is a repo with the problem: https://github.com/twilio/twilio-video-app-ios
Accepted Answer
Hi subclass,

I'm familiar with the pattern you're using to swap out your App Delegate for unit testing. I'm wondering if you've considered using framework-hosted tests, as opposed to app-hosted tests, so your app and scene delegates wouldn't be used at all. There's also a speed improvement to your tests if you take this approach since it won't install your app into a simulator.

Assuming you can't make your tests framework-hosted rather than app-hosted (Which you can enable by changing which host application your test target uses in its General settings), I'll admit I'm not sure if you can configure your scene delegate the way you'd like based on the caching behavior you seem to be noticing, though I have 2 ideas:

1) Make your TestAppDelegate and real AppDelegate conform to both the UIApplicationDelegate and UISceneDelegate protocol, and then use composition to forward the calls into separate implementation classes (so as to maintain the separation of concerns that's ideal with the SceneDelegate and AppDelegate classes we generate for you). I haven't tested this but it seems like, if you made both your AppDelegate and TestAppDelegate handle both protocol conformances, this would work to make sure your test double gets loaded consistently.

2) Alternatively, add a switch on the launch arguments of your iOS app inside the concrete SceneDelegate class to control the path it takes when testing, perhaps via a --unit-testing flag, so you can avoid expensive, stateful, or tricky logic you'd like to avoid for testing. You can again use composition and delegation here to keep the SceneDelegate code manageable. If you're concerned about compiling in test hooks into your app code, you can also conditionally compile this code out for release builds.




Hi subclass

The problem is that application(_:configurationForConnecting:options:) is only called after first app launch.  It is unexpected that the function is not called on every launch. I read the documentation and did not find anything describing when > this function is and is not called: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/3197905-application

Note that this method pertains to the UISceneSession that is connecting. UISceneSessions have a different lifetime than their related UIScene instances. From the comment in UIApplication.h:

Code Block
// All of the representations that currently have connected UIScene instances or had their sessions persisted by the system (ex: visible in iOS' switcher)
@property(nonatomic, readonly) NSSet<UISceneSession *> *openSessions API_AVAILABLE(ios(13.0));


The session can be persisted by the system, yet have a nil UIScene instance because it isn't connected.

The persistence, here, is driven by system behavior and is not something that's really configurable. As such, I think you're seeing expected behavior regarding application(_:configurationForConnecting:options:). There is still a remaining UISceneSession that's persisted, so application(_:configurationForConnecting:options:) is not called.

I think either of the options mentioned by Developer Tools Engineer above would work, but let us know if they don't.

If you haven't already, could you file a Feedback on this? I bet you aren't the first person to want different behavior here, and there may be more we can do in the future to make this better. Thanks.
Thanks everyone for the help! This does give me more ideas that I will try out soon and let you know how it goes.

The feedback ID is 7641071.
Thank you!
I tried out alternative solution #1: make TestAppDelegate and AppDelegate conform to both the UIApplicationDelegate and UISceneDelegate protocol.

It was not successful I believe because line 9 below. It seems I must specify the type of class for UISceneDelegate. And if the session is persisted after I run the app, the session is going to use the production type when I run unit tests because this function isn't called again. In other words if I run the app and then run unit tests, the tests will use TestAppDelegate as the UIApplicationDelegate and AppDelegate as the UIWindowSceneDelegate.

Code Block swift
   func application(
    _ application: UIApplication,
    configurationForConnecting connectingSceneSession: UISceneSession,
    options: UIScene.ConnectionOptions
  ) -> UISceneConfiguration {
    print("Configure production scene delegate.")
    let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    configuration.delegateClass = type(of: self)
    return configuration
  }

I need to think about the other alternatives more.
I haven't had time yet to implement solution #2 (use launch arguments to control path at launch) but I will accept it as the correct answer. I like that it is a universal solution without any major tradeoffs that should work for any app.

It would be even better if there was a nice dependency injection framework for Swift that made swapping dependencies easier. :)
Problem using fake SceneDelegate for unit tests
 
 
Q