Unit Testing UIViewControllers in Swift with iOS 8 and Xcode 6

I spent a good bit of time trying to figure out how to properly unit test ViewControllers. There are a few posts on StackOverflow but there was only one real answer I found (as of Xcode 6 Beta 6). I wasn’t satisfied with that solution and decided to keep digging.

The problem that arises with Swift is that each target makes a module. The way you would test a ViewController in Xcode 5 would be to do something like this:

MyViewController vc;

- (void)setUp {
    [super setUp];
    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    self.vc = [storyboard instantiateViewControllerWithIdentifier:@"main"];
    [self.vc performSelectorOnMainThread:@selector(loadView) withObject:nil waitUntilDone:YES];
}

When you try this in Swift, you get a crash when casting the object returned from the instantiateViewControllerWithIdentifier method. This is because the storyboard is making a ViewController that is actually of type AppModule.MyViewController and you are trying to cast it as UnitTestModule.MyViewController.

The only solution I was able to find while searching was to make MyViewController public along with any methods and properties I wanted to access. I could then import my module into my test class with import AppModule (usually your app name).

The problem with this approach is that you either have to make everything public (which is really bad practice) or make getters and setters for everything you want to test (the whole advantage of properties is to have automated getters and setters).

The solution is actually quite simple. Assuming you have setup all of your ViewControllers and your StoryBoard to have membership of the test target along with the main target, you can simply use a StoryBoard that is part of your test bundle instead of part of the main target’s bundle. So instead of using nil, use NSBundle(forClass: self.dynamicType). I ended up using this for my tests:

import UIKit
import XCTest

class LoginViewControllerTests: XCTestCase {

    var vc : LoginViewController!

    override func setUp() {
        super.setUp()

        var storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
        vc = storyboard.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
        vc.loadView()
    }

    override func tearDown() {
        super.tearDown()
    }

    func testUI() {
        XCTAssertTrue(true, "A passing test")
    }

}

I think that this may have been the “correct” implementation before modules, but I didn’t find anyone doing it this way: presumably because it didn’t affect the tests.

Written on August 19, 2014