Search code examples
swiftmacoscocoa

Swift: How to not load AppDelegate during Tests


I have an OS X application which on startup loads some data from a server and pushes notifications to the NSUserNotificationCenter. Now I have the problem that this also happens during my unit tests. I found no way yet to prevent this. Of course I could stub the HTTP loads. But in some cases I want to test the loading and then the notifications get sent anyway.

What I'm trying to do is to make the test runs not load the AppDelegate but a fake one that I'm only using for tests. I found several examples [1] on how to do that with UIApplicationMain, where you can pass a specific AppDelegate class name. The same is not possible with NSApplicationMain [2].

What I've tried is the following:

Removed @NSApplicationMain from AppDelegate.swift, then added a main.swift with the following content:

class FakeAppDelegate: NSObject, NSApplicationDelegate {
}

NSApplication.sharedApplication()
NSApp.delegate = FakeAppDelegate()
NSApplicationMain(Process.argc, Process.unsafeArgv)

This code runs before tests but has no effect at all.

I might have to say: My AppDelegate is almost empty. To handle the MainMenu.xib stuff I made a separate view controller which does the actual loading and notification stuff in awakeFromNib.

[1] http://www.mokacoding.com/blog/prevent-unit-tests-from-loading-app-delegate-in-swift/

[2] https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/Miscellaneous/AppKit_Functions/#//apple_ref/c/func/NSApplicationMain


Solution

  • After days of trying and failing I found an answer on the Apple forums:

    The problem was that my main.swift file was initializing my AppDelegate before NSApplication had been initialized. The Apple documentation makes it clear that lots of other Cocoa classes rely on NSApplication to be up and running when they are initialized. Apparently, NSObject and NSWindow are two of them.

    So my final and working code in main.swift looks like this:

    private func isTestRun() -> Bool {
        return NSClassFromString("XCTest") != nil
    }
    
    private func runApplication(
        application: NSApplication = NSApplication.sharedApplication(),
        delegate: NSObject.Type?   = nil,
        bundle: NSBundle?          = nil,
        nibName: String            = "MainMenu") {
    
        var topLevelObjects: NSArray?
    
        // Actual initialization of the delegate is deferred until here:
        application.delegate = delegate?.init() as? NSApplicationDelegate
    
        guard bundle != nil else {
            application.run()
            return
        }
    
        if bundle!.loadNibNamed(nibName, owner: application, topLevelObjects: &topLevelObjects ) {
            application.run()
        } else {
            print("An error was encountered while starting the application.")
        }
    }
    
    if isTestRun() {
        let mockDelegateClass = NSClassFromString("MockAppDelegate") as? NSObject.Type
        runApplication(delegate: mockDelegateClass)
    } else {
        runApplication(delegate: AppDelegate.self, bundle: NSBundle.mainBundle())
    }
    

    So the actual problem before was that the Nib was being loaded during tests. This solution prevents this. It just loads the application with a mocked application delegate whenever it detects a test run (By looking for the XCTest class).

    I'm sure I will have to tweak this a bit more. Especially when a start with UI Testing. But for the moment it works.