Unit Testing UIViewControllers
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)
}