How to write unit test cases for UIViewControllers? - ios

Specifically , i want to check the IBOutlets , but by using
var vc = storyboard.instantiateViewControllerWithIdentifier("VCID") as UIViewController
the IBOutlets are nil .

The view controller's view property has to be accessed before the view is loaded and the outlets connected.
let view = vc.view
Will do it, or you can present the view controller on screen (this will also cause viewWill/DidAppear to be called) by setting it as the window's root view controller.

we have to add bundle as NSBundle(forClass: self.dynamicType) instead of nil .
func testClientsStoryBoard(){
let storyboard = UIStoryboard(name: "abc", bundle: NSBundle(forClass: self.dynamicType))
var vc = storyboard.instantiateViewControllerWithIdentifier("abcVC") as abcViewController
vc.loadView()
XCTAssertNotNil(vc.outletName,"Not Nil")
}

Related

How to initialize a View Controllers outlets inside of a Second View Controller?

How would I access a second view controllers IBOutlets from with in a second view controller? In my situation I initialize a current view controller with a statement like this.
let eqController = EqualizerViewController()
But although this statement works, when referencing an outlet from eqController, it succeeds at build time but fails at run time because he outlets are nil. How would I initialize those outlets from within the current view controller? Thanks in advance.
You can't access outlets because they are nil until the Vc loads , so inside the secondVC
class SecondVc:UIViewController {
var sendedStr = ""
}
//
let vc = storyboard.instantiateViewController(withIdentifier: "secondId") as! SecondVc
vc.sendedStr = ""
// here present / push
If you're trying to initialize a view controller to present, you would want to use the storyboard instead of doing the let eqController = EqualizerViewController().
Try this instead:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let eqController = storyboard.instantiateViewController(withIdentifier: "eqController") as? EqualizerViewController
But you will also need to update the view controller's identifier in the storyboard, as shown below:

Initial view controller and Tab View Controller

I'm working with a single view app but also I'm using Tab View controller at one case , but the Tab view Controller must be put as initial view controller to work so how I can use Tab view controller without make it as initial view Controller?
#SamahAhmed
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let diallerTBC = storyboard.instantiateViewControllerWithIdentifier("tabBarStoryboardId") as! MyTabbarController
//create a variable for e.g : data at MyTabbarController and set like
diallerTBC.data = "testing"
When you are clicking on button to move tabbar controller
Provide a storyboard id to tabbar and use below code
Write this as a class method in appdelegate and call it on button click
let storyboard = UIStoryboard.init(name: "Main", bundle: nil)
let diallerTBC = storyboard.instantiateViewControllerWithIdentifier("tabBarStoryboardId") as! UITabBarController
self.window?.rootViewController = diallerTBC
self.window?.makeKeyAndVisible()
}
I hope this will work
Thanks

How to change the initial view controller of storyboard?

I have 2 view controller ,and I disabled the initial view controller of first view controller ,and enabled the second view controller,but when start the project,the initial view controller is still the first view controller ,what should I do? Thanks!
Tap the second view controller, and select "Is initial View Controller" in Attributes inspector.
Alternatively you can do this with code too.
In your AppDelegate class's didFinishLaunchingWithOptions method you can write
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let secondVC = storyBoard.instantiateViewControllerWithIdentifier("SecondViewController") as! SecondViewController
self.window?.rootViewController = secondVC
Edit Swift 3.0
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let secondVC = storyBoard.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
self.window?.rootViewController = secondVC
Assuming that your storyboard name is Main.Storyboard and your SecondViewController Storyboard ID and Restoration ID is also set in Identity Inspector and use Storyboard ID is checked.
If you are starter like me ( both iOS and OSX mechine ) and you wanted to change the entry point within same storyboard, then,
-> long press on the entry point arrow.
-> drag it to the view controller you wish to make the starting point, the arrow will now move to your new screen.

Setting RootViewController in Appdelegate

I have storyboard in my application, but i don't want to set the Main storyboard file base name in info.plist, as well as I don't want to set the entry point i.e Is Initial View Controller in storyboard for any viewcontroller scene.
Though I want to launch the app with some scene from the storyboard, but all shows me a black screen.
When I tried
let testob:testClass = testClass()
self.window?.rootViewController = testob
in AppDelegate, it didn't worked and got the same black screen.
But when I set the Main storyboard file base name in info.plist, and the entry point i.e Is Initial View Controller in storyboard every thing works.
Is there any solution to do so?
You need to instantiate the window and make it key and visible.
let bounds = UIScreen.main.bounds
self.window = UIWindow(frame: bounds)
self.window?.rootViewController = rootViewController
self.window?.makeKeyAndVisible()
You can load a particular view controller from storyboard.
See this answer.
Example:
window?.rootViewController = initialViewController()
where
private func initialViewController() -> UIViewController {
if isUserLoggedIn {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("LoadingViewControllerIdentifier")
} else {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("HomeViewControllerIdentifier")
}
}
After setting self.window?.rootViewController = testob, you have to do the following
[self.window makeKeyAndVisible]
Also, initialize the viewController from storyboard using appropriate method

View Controller TDD

I am trying to add some unit tests to my project to test view controllers. However I seem to be having problems with seemingly simple things. I have created a sample project which I will refer to. https://github.com/pangers/ViewControllerTesting
The sample contains a UINavigationController as the initial view controller. The root view controller of the UINavigationController is FirstViewController. There is a button on FirstViewController that segues to SecondViewController. In SecondViewController there is an empty textfield.
The two tests I am trying to add are:
1) Check button title in FirstViewController is "Next Screen".
2) Check textfield in SecondViewController is empty, "".
I have heard reports of adding your swift files to both the main target and the test target is not good practice. But rather it is better to make whatever you want to access in your tests public and import the main target into the tests. So that is what I have done. (I have also set the "Defines Module" for the main target to YES as that is what I have read in a few articles aswell).
In FirstViewControllerTests I have instantiated the first view controller with the following:
var viewController: FirstViewController!
override func setUp() {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
viewController = navigationController.topViewController as FirstViewController
viewController.viewDidLoad()
}
And I have added the test:
func testCheckButtonHasTextNextScreen() {
XCTAssertEqual(viewController.button.currentTitle!, "Next Screen", "Button should say Next Screen")
}
Similarly, for SecondViewControllerTest, I have set it up using:
var secondViewController:SecondViewController!
override func setUp() {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
let firstviewController = navigationController.topViewController as FirstViewController
firstviewController.performSegueWithIdentifier("FirstToSecond", sender: nil)
secondViewController = navigationController.topViewController as SecondViewController
secondViewController.viewDidLoad()
}
And the test:
func testTextFieldIsBlank() {
XCTAssertEqual(secondViewController.textField.text, "", "Nothing in textfield")
}
They both fail and I am not too sure as to why. My suspicion is that the way I am instantiating the view controllers is not correct. Is the best way to instantiate the view controllers is to use the storyboard (just like it would if it were to run in real life)? Or is it acceptable to be instantiated via:
var viewController = FirstViewController()
What are you guys' experience with TDD and view controllers in swift?
I am using Swift with XCode 6.1.1.
Thanks in advance.
Solved
Ok after considering the answers from modocache and Mike Taverne, I've found my solution and I've learnt a few things which I will write down below.
1) I made anything class/method/variable that I want to test public. I do not need to add the swift files to the test target.
2) I only needed to set "Defines Module" for the "Main" target (as opposed to the "Test" target or the entire project)
3) When instantiating the storyboard, the bundle should be set to nil rather than NSBundle(forClass: self.dynamicType), otherwise tests will fail.
4) As modocache stated, it is good to give your view controller's a StoryboardID and instantiate them like so:
viewController = storyboard.instantiateViewControllerWithIdentifier("FirstViewController") as FirstViewController
However, instantiating the view controller like this ONLY instantiates the view controller alone, and not any navigation controllers that it may be embedded in. That means, attempting to do
XCTAssertFalse(viewController.navigationController!.navigationBarHidden, "Bar should show by default")
will result in a nil exception. I confirmed this with
XCTAssertNil(viewController.navigationController?, "navigation controller doesn't exist")
which resulted in a successful test.
Since I wanted to check the state of the navigation bar in FirstViewController, you must instantiate the view controller like so:
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let navigationController = storyboard.instantiateInitialViewController() as UINavigationController
viewController = navigationController.topViewController as FirstViewController
Now performing the test
XCTAssertFalse(viewController.navigationController!.navigationBarHidden, "nav bar should be showing by default")
results in a successful test.
5) let _ = viewController.view does indeed trigger viewDidLoad() which was confirmed by a test
6) let _ = viewController.view does not trigger viewWillAppear(), and I presume anything afterwards aswell. viewController.viewWillAppear(false/true) needs to be called manually to trigger it (Confirmed by a test).
Hopefully this will be of help to people. I will push the updated project to GitHub (link above) if anyone would like to play around with it.
Update #2
After all the above, I still could not figure out how to transition from the first view controller to the second view controller (so that I may test navigation bar properties in SecondViewControllerTests.swift). I tried
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
let firstVC = nc.topViewController as FirstViewController
firstVC.performSegueWithIdentifier("FirstToSecond", sender: nil)
secondVC = nc.topViewController as SecondViewController
which caused an error.
I also tried
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
let firstVC = nc.topViewController as FirstViewController
firstVC.toSecondVCButton.sendActionsForControlEvents(UIControlEvents.TouchUpInside)
secondVC = nc.topViewController as SecondViewController
which did not work.
I eventually tried
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let nc = storyboard.instantiateInitialViewController() as UINavigationController
vc = storyboard.instantiateViewControllerWithIdentifier("Second") as SecondViewController
nc.pushViewController(vc, animated: false)
let _ = vc.view
vc.viewWillAppear(false)
which worked perfectly with my tests (allowed me to access navigation bar properties)!
I agree with #MikeTaverne's answer: I prefer accessing -[UIViewController view] in order to trigger -[UIViewController viewDidLoad], rather than calling it directly. See if the test failures for FirstViewController go away once you use this instead:
viewController = navigationController.topViewController as FirstViewController
let _ = viewController.view
I'd also recommend giving both view controllers identifiers in your storyboard. This will allow you to instantiate them directly, without accessing them via UINavigationController:
var secondViewController: SecondViewController!
override func setUp() {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
secondViewController = storyboard.instantiateViewControllerWithIdentifier("SecondViewController")
as SecondViewController
let _ = secondViewController.view
}
Check out my talk on testing UIViewController at Brooklyn Swift for details: https://vimeo.com/115671189#t=37m50s (my presentation begins around the 37'50" mark).
I've begun unit testing view controllers recently, and it poses some unique challenges.
One challenge is getting the view to load. Looking at your set up for FirstViewController, you are trying to do this with viewController.viewDidLoad().
My suggestion is to replace that line with this:
let dummy = viewController.view
Accessing the .view property will force the view to load. This will trigger the .viewDidLoad in your ViewController, so don't call that method explicitly in your test.
This approach is considered hacky by some people, but it is simple and effective. (See Clean way to force view to load subviews early)
As an aside, I am finding the best way to test view controllers is to move as much code out of the view controllers as possible into other classes that are more easily tested.
If your view controller is defined in a storyboard, then you need to instantiate it that way for your outlets to be set up properly. Trying to initialize it like an ordinary class won't work.

Resources