17

Due to a couple issues, I want the XCTest target in a project to run a separate app delegate. Using ObjC, this was a relatively straightforward process: manipulate main.m (see: https://stackoverflow.com/a/15725328/1299041).

Since it seems that a Swift application is initialized with @UIApplicationMain in the AppDelegate, is it possible to initialize with a separate AppDelegate for the test target?

Community
  • 1
  • 1
Whoa
  • 1,334
  • 11
  • 26

3 Answers3

18

It's strongly unrecommended to add conditions to normal code checking if its being tested. Instead you should mock your AppDelegate in tests to do whatever you want.

Then you could replace delegate of UIApplication is setUp in super class of your each XCTestCase'es.

class MockAppDelegate:NSObject, UIApplicationDelegate {

}


class BaseTest: XCTestCase {

    override func setUp() {
        super.setUp()
        UIApplication.shared.delegate = MockAppDelegate()
       }
}
class Test1: BaseTest {

    override func setUp() {
        super.setUp()
        // normal testing
       }
}

If you still want to stop code execution for tests this is my method that works well:

You can add startup parameter to app which indicates that this is test run App Start execution

These parameters are accessible from NSUserDefaults

#define IS_TESTS [[NSUserDefaults standardUserDefaults] boolForKey:@"TESTING"]
Greg Hilston
  • 2,397
  • 2
  • 24
  • 35
Bartosz Hernas
  • 1,130
  • 1
  • 9
  • 17
  • 4
    This is a good point, I agree with refraining from adding test related conditions to the normal code. What I'm actually trying to do is stop code from executing in the main `AppDelegate` (dealing with user & session state, and data storage). Unfortunately, creating a `MockAppDelegate` doesn't accomplish that. – Whoa Jan 16 '15 at 16:54
  • I edited my answer, there is a way to check this also in Swift. Just put `if` statement in your app delegate. – Bartosz Hernas Jan 17 '15 at 20:22
  • 7
    This won't prevent normal app delegate to run, will it? I mean, normally AppDelegate's `didFinishLaunchingWithOptions` is fired before the test suites start to run. – Marc-Alexandre Bérubé Oct 30 '15 at 12:22
  • When you run tests, you pass arguments on launch which you access from NSUserDefaults. So answering your question: it will prevent AppDelegate to run as this parameter is known already before test suites start to run. – Michał Hernas Nov 05 '15 at 15:14
  • 2
    @MichałHernas @Marc-AlexandreBérubé I'm trying this out and it runs the normal app delegate - `setUp()` is executed after the normal app delegate run `didFinishLaunchingWithOptions`. My test delegate is never used. – User Aug 18 '17 at 08:30
  • The delegate is a unowned reference, so this assignment is unsafe and can lead to obscure crashes. Make ```MockAppDelegate()``` a member, a global, or create a strong reference to it some other way. – MattD Nov 13 '17 at 16:59
  • @MattD Can you improve the accepted answer or submit your own? I ask because @Marc-AlexandreBérubé and @Ixx are right, the accepted answer this calls `AppDelegate` – Greg Hilston Aug 08 '18 at 14:02
  • I don't see an accepted answer here (no green check mark). Am I missing it? As for my comment, it applies to this answer which will work for post-launch rebinding of the AppDelegate. If that is unacceptable, then @awolf's answer is better. – MattD Aug 09 '18 at 15:14
9

To achieve this is Swift you need to take a couple of steps:

  1. If you are using Storyboards, create your view stack programmatically on your AppDelegate.

    Remove Main.storyboard from your project configuration Project configuration

    Delete @UIApplicationMain from the beginning of your AppDelegate and add this code.

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
       let storyboard = UIStoryboard(name: "Main", bundle: nil)
       let vc = storyboard.instantiateInitialViewController()
    
       let window = UIWindow(frame: UIScreen.main.bounds)
       window.rootViewController = vc
    
       window.makeKeyAndVisible()
       self.window = window
    
       return true
    }
    
  2. Create a new file at the root your target and call it main.swift.

    Add this code if you don't need to do any setup for your tests

    import UIKit
    
    let kIsRunningTests = NSClassFromString("XCTestCase") != nil
    let kAppDelegateClass = kIsRunningTests ? nil : NSStringFromClass(AppDelegate.self)
    
    UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, kAppDelegateClass)
    

    If you need to make some configuration before you run the tests, create a new class FakeAppDelegate as a subclass from NSObject and add your setup code there.

    Put this code in main.swift

    import UIKit
    
    let kIsRunningTests = NSClassFromString("XCTestCase") != nil
    let kAppDelegateClass = kIsRunningTests ? NSStringFromClass(FakeAppDelegate.self) : NSStringFromClass(AppDelegate.self)
    
    UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, nil, kAppDelegateClass)
    
andecoder
  • 183
  • 1
  • 6
  • Dude, it worked!!! Finally I can run my unit tests without the whole app running. If you get any updates to this method I'd love to seem them. – 23inhouse Nov 02 '19 at 12:49
2

This solution to this is as follows:

  1. Duplicate your existing application's Target and rename it to something appropriate. In your case maybe 'TestingHarness' or some such. Note that you'll also want to change the bundle identifier and rename the corresponding Info.plist file. Renaming the Info.plist file means you'll need to change the Info.plist filename setting in your new target's Build Settings tab to match the new name.

  2. Create another *AppDelegate.swift file. In your case I'd call it TestAppDelegate.swift.

  3. Copy over your existing AppDelegate.swift file's contents into TestAppDelegate.swift and edit as desired. Make sure to leave the @UIApplicationMain annotation and implement the needed UIApplicationDelegate callbacks.

  4. Change the target membership of each of your *AppDelegate.swift files so that AppDelegate.swift is not included in your new 'TestHarness' target and TestAppDelegate.swift is not included in your main app's target. (You edit a file's Target Membership by selecting it in the File Browser and opening the File Inspector which you can access in the right-sidebar by default, or by choosing it in the menu under View -> Utilities.)

  5. Now you have two separate targets with separate App Delegates that you can build and run independently. The final step is to select your new 'TestHarness' target as the Host Application for your test targets. (Click the top-level project entry in the File Browser, then click your desired test Target in the sub-listing. On the General tab you'll see Host Application as the only available drop down.)

Note: these instructions are for Xcode 7.2.

awolf
  • 1,862
  • 22
  • 29