16

In Swift, Xcode6-Beta5, I'm trying to unit test my "ViewController", not a very creative name.

From looking at other responses, I'm thinking I don't have my "Tests" target configured properly.

Failing test code:

  func testItShouldLoadFromStoryboard() {

    var storyBoard: UIStoryboard?
    var anyVC: AnyObject?
    var viewController: ViewController?
    var uiViewController: UIViewController?

    storyBoard = UIStoryboard(name:"Main", bundle: nil)
    XCTAssert(storyBoard != nil, "Test Not Configured Properly")

    anyVC = storyBoard?.instantiateInitialViewController()
    viewController = anyVC  as? ViewController
    // Failing Assertion 
    XCTAssert(viewController != nil, "Test Not Configured Properly")

    uiViewController = anyVC as? UIViewController
    XCTAssert(uiViewController != nil, "Test Not Configured Properly")

  }

I can force the cast with the following lines:

anyVC = storyBoard?.instantiateInitialViewController()
viewController = (anyVC != nil) ? (anyVC  as ViewController) : nil

But this caused the following crash:

libswiftCore.dylib`swift_dynamicCastClassUnconditional:
0x10724f5a0:  pushq  %rbp
0x10724f5a1:  movq   %rsp, %rbp
0x10724f5a4:  pushq  %r14
0x10724f5a6:  pushq  %rbx
0x10724f5a7:  movq   %rsi, %rbx
0x10724f5aa:  movq   %rdi, %r14
0x10724f5ad:  testq  %r14, %r14
0x10724f5b0:  je     0x10724f5de               ; swift_dynamicCastClassUnconditional + 62
0x10724f5b2:  movabsq $-0x7fffffffffffffff, %rax
0x10724f5bc:  andq   %r14, %rax
0x10724f5bf:  jne    0x10724f5de               ; swift_dynamicCastClassUnconditional + 62
0x10724f5c1:  movq   %r14, %rdi
0x10724f5c4:  callq  0x107279a6e               ; symbol stub for: object_getClass
0x10724f5c9:  nopl   (%rax)
0x10724f5d0:  cmpq   %rbx, %rax
0x10724f5d3:  je     0x10724f5ed               ; swift_dynamicCastClassUnconditional + 77
0x10724f5d5:  movq   0x8(%rax), %rax
0x10724f5d9:  testq  %rax, %rax
0x10724f5dc:  jne    0x10724f5d0               ; swift_dynamicCastClassUnconditional + 48
0x10724f5de:  leaq   0x3364d(%rip), %rax       ; "Swift dynamic cast failed"
0x10724f5e5:  movq   %rax, 0xa456c(%rip)       ; gCRAnnotations + 8
0x10724f5ec:  int3   
0x10724f5ed:  movq   %r14, %rax
0x10724f5f0:  popq   %rbx
0x10724f5f1:  popq   %r14
0x10724f5f3:  popq   %rbp
0x10724f5f4:  retq   
0x10724f5f5:  nopw   %cs:(%rax,%rax)

I've also successfully instantiated the ViewController directly but that doesn't do the IBOutlet processing, which is one of the purposes of my tests, to make sure I don't break the linking by renaming things, deleting connections in the Storyboard editor, or the many other ways I have found to break things ...

EDIT ---- I started with a fresh project, chose iOS Application -> Single View Application template.

Replaced the testExample with the code as shown below, Added ViewController to the test target. Similar results. This template has a single view controller of type ViewController, nothing else in the storyboard.

  func testExample() {


      var storyBoard: UIStoryboard?
      var anyVC: AnyObject?
      var viewController: ViewController?

      storyBoard = UIStoryboard(name:"Main", bundle: nil)
      XCTAssert(storyBoard != nil, "Test Not Configured Properly")

      anyVC = storyBoard?.instantiateInitialViewController()
      viewController = anyVC as? ViewController
      XCTAssert(viewController != nil, "Test Not Configured Properly")

      // This is an example of a functional test case.
      XCTAssert(true, "Pass")

    }

The following lldb output at a break point just after the value of anyVC is set:

(lldb) po anyVC
(instance_type = Builtin.RawPointer = 0x00007fe22c92a290 -> 0x000000010e20bd80 (void *)0x000000010e20bea0: OBJC_METACLASS_$__TtC22TestingViewControllers14ViewController)
 {
  instance_type = 0x00007fe22c92a290 -> 0x000000010e20bd80 (void *)0x000000010e20bea0: OBJC_METACLASS_$__TtC22TestingViewControllers14ViewController
} 
ahalls
  • 1,095
  • 1
  • 12
  • 23
  • It looks like `anyVC` is not of type `ViewController`, so are you sure that the Initial View Controller in your storyboard is actually of that type? Make sure to (1) check the initial view controller's class in the identity inspector and (2) set a breakpoint after `instantiateInitialViewController()` and have a look at `anyVC`'s type. Edit your question with the information you found. – knl Aug 17 '14 at 06:39
  • I might be jumping the gun: rdar://18043164 – ahalls Aug 17 '14 at 08:04
  • The problem, it seems to me, is that the type of the ViewController in the storyboard is App.ViewController, which is different from ViewController. Remember that swift has modules. When you linked ViewController into your test target you effectively created two versions of the class: App.ViewController and AppTests.ViewController and they are not the same (they won't cast properly). I couldn't figure out how to reference a Swift class from the app target in the test target. There must be a way, though. Hope that's helpful. – Derrick Hathaway Aug 18 '14 at 15:23
  • 2
    Adding, `println(anyVC)`, `println(ViewController())` after your `instantiateInitialViewController` call illustrates the difference. – Derrick Hathaway Aug 18 '14 at 15:33
  • @DerrickHathaway thanks for pointing that out. I'm having trouble with this as well. I see when I use your `println()` statements that I'm getting Target.ViewController which is likely the source of the problem. – Mike Cole Aug 18 '14 at 20:52

3 Answers3

16

I came up with a better solution than making everything public. You actually just need to use a storyboard that comes from the test bundle instead of using nil (forcing it to come from the main target's bundle).

var storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: NSBundle(forClass: self.dynamicType))
vc = storyboard.instantiateViewControllerWithIdentifier("LoginVC") as LoginViewController
vc.loadView()
Mike Cole
  • 1,291
  • 11
  • 19
  • 2
    Yes ... this is a much better answer. But please put the essence of the answer here ... I think it fits the StackOverflow style better ... let storyBoard = UIStoryboard(name:"Main", bundle: NSBundle(forClass: self.dynamicType)) – ahalls Aug 19 '14 at 21:18
  • You are right. I was in a hurry earlier. I have updated the answer to include the important part of the post. – Mike Cole Aug 20 '14 at 01:20
  • In Obj-C:---- [[UIStoryboard storyboardWithName:@"STORYBOARDNAME" bundle:[NSBundle mainBundle]] instantiateViewControllerWithIdentifier:@"IDENTIFIER"]; – cynistersix Feb 20 '15 at 02:56
  • loadView() is a private API method and not considered good coding practice, it is also likely that the app would be rejected from the app store by apple for implementing this method. – Jacob King Aug 05 '15 at 10:08
  • @JacobKing, ```loadView()``` is not really 'private'. You are right in that you typically shouldn't call it in your main target, but this is specifically for testing. Calling ```loadView()``` can be useful in testing because you may want to load a single ```ViewController``` without the context of the rest of the app. – Mike Cole Aug 12 '15 at 14:48
0

Thanks @Derrik Hathaway for the key to the solution. The application and the test target are different modules.

My application has the unfortunate name of TestingViewControllers To correct the issue I made the following changes and made the ViewController a public class back in the Application code.

Added this line to test Case:

import TestingViewControllers

Change the test case to this:

func testExample() {


  var storyBoard: UIStoryboard?
  var anyVC: AnyObject?
  var viewController: TestingViewControllers.ViewController?

  storyBoard = UIStoryboard(name:"Main", bundle: nil)
  XCTAssert(storyBoard != nil, "Test Not Configured Properly")

  anyVC = storyBoard?.instantiateInitialViewController()
  viewController = anyVC as? TestingViewControllers.ViewController
  XCTAssert(viewController != nil, "Test Not Configured Properly")

  // This is an example of a functional test case.
  XCTAssert(true, "Pass")

}

Test case passes

ahalls
  • 1,095
  • 1
  • 12
  • 23
0

Have you set the storyboardID if you have then you can cast it like this.

You need also rename your view controller class to for example here MyAppViewController make main storyboard use it instead of the standard viewcontroller class then set the storyboardID to for example MyappVC

  var sut : MyAppViewController!

override func setUp() {
    super.setUp()

    // get a reference to storyboard and the correct
    // viewcontroller inside it. Remember to add
    // storyboardID in this case "MyappVC"


    let StoryBoard = UIStoryboard.init(name: "Main", bundle: nil)
    sut = StoryBoard.instantiateViewControllerWithIdentifier("Myapp VC")as!
    CalculatorViewController

    //trigger viewDidLoad()
    _ = sut.view
wpj
  • 206
  • 2
  • 14