Swift - Unit testing functions that involve IBOutlets?
Asked Answered
R

6

13

I am setting up unit testing for my Swift project and am having trouble testing a class function that involves updating IBOutlets.

I have a function, validateUrl, which expects a string to be passed, then validates it. If it is valid, it enables a UIButton, if it is invalid, it disables a UIButton. When I run a test that calls this function, the app crashes on the line of code that enables or disables the UIButton. The storyboard and controllers both has the proper Test target set.

This line of code:

    self.submitButton.enabled = true// Enable Submit Button

Spits out this error:

fatal error: unexpectedly found nil while unwrapping an Optional value
Ruff answered 4/11, 2014 at 17:6 Comment(2)
What sort of class are you testing? Is it an UIViewController subclass?Derrick
@Derrick - Correct. The class is a subclass of UIViewController in Swift.Ruff
C
10

You have to initiate the view controller using the storyboard. See the documentation here: https://developer.apple.com/library/ios/documentation/uikit/reference/UIStoryboard_Class/index.html#//apple_ref/occ/instm/UIStoryboard/instantiateViewControllerWithIdentifier:

If you initialize the view controller directly, it will not have any connections because the VC itself does not know of the storyboard in this case.

Cue answered 4/11, 2014 at 17:29 Comment(1)
can we do that same using a nib?Sparrow
T
21

Try this code to initialize the IbOutlets of your view controller:

   let yourStoryboard = UIStoryboard(name: "Your_storyboard", bundle: nil)
   yourViewController = yourStoryboard.instantiateViewController(withIdentifier: "YourViewController") as! YourViewController
   yourViewController.loadView() // This line is the key
Tears answered 31/5, 2018 at 7:51 Comment(1)
loadView() did the trick for me. I did not have to instantiate my storyboard view since it was connected to my previous storyboard VC.Throw
C
10

You have to initiate the view controller using the storyboard. See the documentation here: https://developer.apple.com/library/ios/documentation/uikit/reference/UIStoryboard_Class/index.html#//apple_ref/occ/instm/UIStoryboard/instantiateViewControllerWithIdentifier:

If you initialize the view controller directly, it will not have any connections because the VC itself does not know of the storyboard in this case.

Cue answered 4/11, 2014 at 17:29 Comment(1)
can we do that same using a nib?Sparrow
W
2

You may need to add the controllers view to a hierarchy prior to testing to force the controller to load the XIB

let localContainer = UIView(frame:someFrame)
let controllerUnderTest = //instantiate your controller
localContainer.addSubview(controllerUnderTest.view)

//at this point you can test outlets

Otherwise your outlets will be nil as they haven't been connected yet.

Wylie answered 4/11, 2014 at 17:32 Comment(1)
This can also be done by simply accessing the view property: _ = localController.viewAnnorah
S
2

Testing IBOutlet's is not the best approach, because:

  • outlets should be considered implementation details, and should be declared as private
  • outlets are most of the time connected to pure UI components, and unit tests deal with testing the business logic
  • testing the value injected into the outlet by another function can be considered somewhat as integration testing. You'd also double the unit tests you have to write by having to test the connected/unconnected outlet scenarios.

In your particular case, I'd recommend instead to test the validator function, but first making it independent of the controller class (if it's not already). Having that function as an input->output one also bring other benefits, like increased reusability.

Once you have tested all the possible scenarios for the validator, validating that the outlet correctly behaves it's just a matter of a quick manual testing: just check if the outlet behaves like the validator returned. UI stuff are better candidates for manual testing, as manual testing can catch other details too (like positioning, colors, etc).

However, if you really want to test the outlet behaviour, one technique that falls into the unit testing philosophy is snapshot testing. There are some libraries available for this, I'd recommend the one from https://github.com/uber/ios-snapshot-test-case/.

Selmaselman answered 31/1, 2019 at 6:44 Comment(2)
great answer! but I can not understand how this validator function should look like. For example I have some logic which setting IBOutlet button to enable/disable state. As you said we can't (shouldn't ) do that directly. So what should I test in this validator function in this case. Thanks!Katerinekates
@Katerinekates for example you can have a function. func isSomethingAvailable(), and from the result of this function you enable/disable some UI components.Selmaselman
H
1

A solution I'm using to test classes in Swift is to create the outlets in the setUp() method of the test. For example, I have a UIViewController subclass that has a UIImageView as an outlet. I declare an instance of my View controller subclass a property of my test class, & configure it's outlet in the setUp() method by initializing a new UIImageView.

var viewController = ViewController() // property on the test class

override func setUp() {
    super.setUp()
    viewController.imageView = UIImageView(image: UIImage(named: "Logo")) // create a new UIImageView and set it as the outlet object
}

Any other outlets can similarly be set in the setUp method, and this way you don't even have to instantiate a storyboard (which, for some reason, despite the fact that I was able to instantiate a UIStoryboard object in my tests, the view controller outlets were still nil).

Hitherward answered 10/11, 2014 at 6:1 Comment(0)
S
0

@talzag In your case iboutlets are nil because they are weak variables, They are supposed to deallocate immediately after instantiation.

Schwab answered 31/1, 2019 at 6:17 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.