Originally, the iOS and macOS development community didn’t have a strong history of unit testing. However, the tools and frameworks have vastly improved over the years.

The tools have improved enough that I now write my UIViewControllers in a test-first manner. There are a couple tricks that are required to pull this off. Here is the boilerplate code I use to unit test:

class ViewControllersTests: XCTestCase {
    
    var sut: ViewController!
    
    override func setUp() {
        super.setUp()
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        sut = storyboard.instantiateInitialViewController() as? ViewController
        UIApplication.shared.keyWindow?.rootViewController = sut
    }
    
    override func tearDown() {
        sut = nil
        super.tearDown()
    }
    
    func testViewNotNil() {
        XCTAssertNotNil(sut.view)
    }
}

There are a few things to note. First, I always name the controller sut for system under test. And this variable is always an implicitly unwrapped optional. I really do want the application to crash if this variable is nil.

var sut: ViewController!

Second of all is the setUp function. The sut almost always gets loaded from a storyboard. I know that this is a controversial decision in the community. Some prefer to programatically build there views. I find that being able to verify an Interface Builder view using unit tests is a reasonable tradeoff.

Also, you should never call loadView. Assigning sut as the rootViewController forces UIKit to run all of the view lifecycle code.

override func setUp() {
    super.setUp()
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    sut = storyboard.instantiateInitialViewController() as? ViewController
    UIApplication.shared.keyWindow?.rootViewController = sut
}

I want to reset the sut after every unit test so that there isn’t any transient state that effects other unit tests.

override func tearDown() {
    sut = nil
    super.tearDown()
}

I finish with always asserting that the main view of the UIViewController is not nil. This is one more check to ensure the view lifecycle correctly executed.

func testViewNotNil() {
    XCTAssertNotNil(sut.view)
}