8

When I call

UIApplication.shared.keyWindow

to try and set the root view controller in my test class, the key window returns nil. Why is this happening?

Here's how I set up my storyboard:

let testBoard = UIStoryboard(name: "TestStoryboard", bundle: Bundle(for: type(of: self)))
let vc = testBoard.instantiateViewController(withIdentifier: "TestController")

UIApplication.shared.keyWindow?.rootViewController = vc

_ = vc.view
vc.viewDidLoad()
jahoven
  • 139
  • 1
  • 7
  • Not sure but I think it's because the window is `nil` by default and you haven't instantiated window yet. See [here](https://stackoverflow.com/questions/34159160/why-is-appdelegate-swift-window-an-optional/41334428#41334428) – mfaani Jan 25 '17 at 19:34
  • btw why are you using `keyWindow`? try using `window` instead and see if that works or is that you are building a macOS app? – mfaani Jan 25 '17 at 19:40
  • I looked at the link but I don't see a solution anywhere. I can't set the `UIApplication.shared.keyWindow` property because it's read only. How do I instantiate the window? – jahoven Jan 25 '17 at 19:43
  • 1
    Again. Can you tell me why u r using `keyWindow` and not `window`? is it a mac Application? (FWIW I'm not answering because I'm still learning Swift + TDD, just commenting with limited knowledge :D) – mfaani Jan 25 '17 at 19:45
  • @Honey There is no `window` property, there's a `windows` array but no `window` object, only `keyWindow` – jahoven Jan 25 '17 at 19:46
  • 1
    So I didn't know that keyWindow is: *This property holds the UIWindow object in the windows array that is most recently sent the makeKeyAndVisible() message.* AppDelegate comes with a pre-build Optional window property, but you the developer can add many more to it. – mfaani Jan 25 '17 at 19:50
  • In addition to the accepted answer you could also do `let window = UIApplication.shared.delegate?.window ?? nil; window?.rootViewController = sut; window?.makeKeyAndVisible(); _ = sut.view;` – mfaani Jan 25 '17 at 20:09

2 Answers2

11

Create a window and assign the view controller.

let testBoard = UIStoryboard(name: "TestStoryboard", bundle: Bundle(for: type(of: self)))
let vc = testBoard.instantiateViewController(withIdentifier: "TestController")

let window = UIWindow()
window.rootViewController = vc
window.makeKeyAndVisible()

vc.view.layoutIfNeeded()

// Add test here

I notice after that you're also calling vc.view and viewDidLoad. I'd recommend just accessing the view to get it to load and not calling viewDidLoad implicitely - personally I use vc.view.layoutIfNeeded()

Depending on what you actually need to test, for me it's very rare to have to assign the view controller to the window itself. You can normally get away with just creating an instance of the view controller, and if you're testing any of the UI code also ensuring the view is populated.

One of the only times I've needed to assign the view controller to a window is when testing things like navigation, where I want to assert that another view controller is being presented modally due to some action.

InsertWittyName
  • 3,930
  • 1
  • 22
  • 27
  • You are exactly correct, that's what I'm doing. I'm trying to present a view controller so I have to set the window. So how do I assign the view controller to the window so I can present other views? – jahoven Jan 25 '17 at 19:48
  • isn't there anyway that you can instantiate the window from AppDelegate? – mfaani Jan 25 '17 at 19:51
  • I'm running a Unit Test so I'm trying to avoid changing any code external to the test file. – jahoven Jan 25 '17 at 19:53
  • I've updated the code to make it more applicable to your sample. – InsertWittyName Jan 25 '17 at 19:57
  • Yea you should also mention what @Honey mentioned in his comment: _This property holds the UIWindow object in the windows array that is most recently sent the makeKeyAndVisible() message._ Specifically the section about makeKeyAndVisibile – jahoven Jan 25 '17 at 20:45
0

I had a similar problem when testing UI inside a library that required access to a real UIWindow but had no test app of it's own.

My workaround was to add a dummy iOS single view target, then assign that as the unit tests host application.

This of course gives your test code a UIApplication singleton complete with delegate and window to play with.

import XCTest
import UIKit

class ExampleTestCase: XCTestCase {
    var rootViewController: UIViewController!

    override func setUp() {
        guard let mainWindow = UIApplication.shared.delegate?.window,
            let window = mainWindow else {
                fatalError("Unable to find window")
        }
        rootViewController = UIViewController()
        window.rootViewController = rootViewController

        setupView()

        window.makeKeyAndVisible()
        RunLoop.current.run(until: Date())
    }
    func setupView() {
        rootViewController.view.backgroundColor = .red
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        label.text = "Hello!"
        label.textColor = .white
        label.font = UIFont.systemFont(ofSize: 25)
        //Do something with window
        rootViewController.view.addSubview(label)
        label.translatesAutoresizingMaskIntoConstraints = false
        label.centerXAnchor.constraint(equalTo: label.superview!.centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: label.superview!.centerYAnchor).isActive = true
    }

    func testExample() {
        print("Im a useless test!")
    }
}

Setup host app

enter image description here

Chris Birch
  • 2,041
  • 2
  • 19
  • 22