Awhile back I blogged about how to test storyboards by loading the ViewController from a Storyboard. Recently I’ve been trying to figure out how to do the same thing in Swift.
Update: latest code appears to be working ok for testing view controllers as long as I don’t try to run them on an actual iPhone device. When I do that, the test hangs.
The code and instructions posted by Mike Cole appear to almost work in Swift with the latest Xcode (I’m using Xcode 6.2, but 6.1 works also). I had to also reference the ViewController’s view. Evidently the ViewController will lazy load the view, meaning that the outlets don’t become connected until after ViewController.view is referenced.
Also, be careful what you put into your view controller’s viewDidLoad method. Code that requires that the view is actually being displayed should be moved to viewWillDisplay or may cause the unit test to crash (since there isn’t really a display during unit testing).
Example XCTest setup code to load a viewcontroller named “MainVC” from main.storyboard.
override func setUp() { super.setUp() let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType)) vc = storyboard.instantiateViewControllerWithIdentifier("MainVC") as ViewController let dummy = vc.view // force loading subviews and setting outlets vc.viewDidLoad() }
I’ve left the previous post below, but it’s basically outdated now. After running into various problems, I resorted to using Objective-C unit tests, and describe how to do so below. I wouldn’t recommend doing so anymore, since Swift tests appear to work ok now.
After fumbling around for awhile with the Swift test case file, and not making much progress, I decided to try something different, and tried adding an Objective-C test case file to my Swift project. At least in this case, I know how to load the ViewController from the Storyboard. And this actually worked.
This turned out to be fairly simple. Let me warn you though, that I consider this approach to be a temporary hack, albeit an interesting one. I believe the correct solution will be to figure out how to do this in a Swift unit test case file. At this point though, I’m not sure if I’m being prevented from doing so due to bugs in beta level Xcode 6, or just my lack of Swift knowledge.
But caveat emptor aside, here is how to create an Objective-C unit test case file in an otherwise entirely Swift project, and use it to load and test a ViewController from a storyboard:
- Set the product module name so that you will know the swift header file name to import (eg. “#import ProductModuleName-swift.h”). Note that this should not be necessary, as the Product Module Name is supposed to default to the Product Name. This wasn’t working as of Beta 2.
- Add an Objective-C unit test case file. Make sure to set the language to Objective-C. It will default to Swift.
- I did not have to create an Objective-C Bridging Header for the Swift ViewController, because I’m only going the other way, from Swift to Objective-C, but I did anyways when prompted. I don’t think having it will hurt anything, and you might want it later.
- Import the swift header file into the Objective-C unit test case file:
#import “ProductModuleName-swift.h”
That was it. This then gave me access to my ViewController definitions. Then use the previous described method of loading a storyboard and viewcontroller from it.
I will update this article once I figure out how to do it correctly in a Swift unit test case file.
Hi Ron,
I’m also trying to test the storyboard, and I tried this with success:
var storyboard = UIStoryboard(name: "Main", bundle: nil)
var controller = storyboard.instantiateViewControllerWithIdentifier("contentViewController") as UINavigationController
controller.viewDidLoad()
What have you tried?
Hi Glauco,
Thanks for the info. I am close to having that working also, but getting an exception in swift_dynamicCast when trying to instantiateViewControllerWithIdentifier.
I’m trying to cast to the swift class MyViewController, not UINavigationController as you are. My app is a single view app. Otherwise code is the same.
Hi Glauco and Ron,
Ron’s code worked for me using Xcode 6 Beta 3 with a few changes.
I set up an ivar in the test case:
var vc:ViewController = ViewController()
I then have in setUp():
let storyboard:UIStoryboard = UIStoryboard(name:"Main", bundle:nil)
self.vc = storyboard.instantiateViewControllerWithIdentifier("TestVC") as ViewController
self.vc.viewDidLoad()
where ViewController is the class I’m testing.
I had some casting problems with Core Data
NSManagedObject
subclasses, too. Have to test Storyboard later today.Solution for Core Data was to:
– remove test target membership of the subclass
–
import YourProjectModuleName
in the test file(s)– make public (i.e. out-of-module access) what should be tested
This worked. And it made me think more carefully about the object’s design because I wouldn’t want to make everything public for the sake of testing.
Thank you Ron for this. I am relatively new to unit testing and iOS development. That’s probably why I struggled a lot with this. However I figured it out.
To prevent others having the same issues I created tutorial https://github.com/SnappedCoffee/UnitTestViewControllerWithSwiftMiniTutorial
Would be great to get your thoughts and feedback on this.
Hello!
I’m new to the testing world!
Should we test UI in unit testing or in integration tests (for example using UI Automation)?
Thanks.