Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to typecast a subclassed UIViewController in Swift?

Tags:

ios

swift

testng

I am trying to test an subclass of UITableViewController in swift with storyboard. I am able to get a reference to the view controller and log it, but I am unable to typecast it to the class I am trying to test, so I can't access methods, properties etc.

What's the proper way of getting a hold of the instance of my class?

Test Scenario:

import XCTest
import UIKit    
class GameListControllerTest: XCTestCase {
    var storyboard = UIStoryboard(name: "Main", bundle: nil)
    var _sut: AnyObject?
    var sut: AnyObject {
        if(_sut?) {
            return _sut!
        }
        _sut = storyboard.instantiateViewControllerWithIdentifier("GameListController")
        return _sut!
    }
    override func setUp() {
        super.setUp()
        println()
        println("=================================")
        println("sut  \(sut)")
        println("view  \(sut.view)")
        println("=================================")

    }
    func testInstance() {
        var vc1 = sut as UITableViewController
        var vc2 = sut as? GameListController
        println()
        println("=================================")
        println("VC1  \(vc1)")
        println("VC2  \(vc2)")
        println("=================================")
    }
}

Output:

XCTestOutputBarrierTest Suite '_TtC15mytabletopTests22GameListControllerTest' started at 2014-06-16 06:13:30 +0000
XCTestOutputBarrierTest Case '-[_TtC15mytabletopTests22GameListControllerTest testInstance]' started.
XCTestOutputBarrier
=================================
sut  <_TtC10mytabletop18GameListController: 0xb338d50>
view  <UITableView: 0xc033800; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0xb344960>; layer = <CALayer: 0xb33c7d0>; contentOffset: {0, 0}; contentSize: {480, 0}>
=================================

=================================
VC1  <_TtC10mytabletop18GameListController: 0xb338d50>
VC2  nil
=================================
Test Case '-[_TtC15mytabletopTests22GameListControllerTest testInstance]' passed (0.007 seconds).
XCTestOutputBarrierTest Suite '_TtC15mytabletopTests22GameListControllerTest' passed at 2014-06-16 06:13:30 +0000.
     Executed 1 test, with 0 failures (0 unexpected) in 0.007 (0.010) seconds

If I try to force typecast (sut as GameListController) I get an runtime exception.

like image 375
Irae Carvalho Avatar asked Dec 25 '22 07:12

Irae Carvalho


1 Answers

Here is what's happening. It took me days to figure out, but my problem was on how my classes were being exported to the targets:

File Inspector Target Membership Incorrect vs. Correct

This was causing two binary copies of my class, one in the app target and another in the test target. If we pay more attention to the logs we might notice:

Test Case '-[_TtC15mytabletopTests22GameListControllerTest testInstance]'

The above is the test method testInstance and it's part of mytabletopTests execution context. Now lets take a look at the instance pulled from Storyboard:

sut  <_TtC10mytabletop18GameListController: 0xb338d50>

This in turn is running on the mytabletop context. This explains why the tests are unable to typecast to GameListController. The GameListController that it knows of is the one compiled inside the test target.

Since removing the class from the test target makes the class unknown to my test case, now I need to import my app target into the test case:

import XCTest
import UIKit
import mytabletop // LINE ADDED

class GameListControllerTest: XCTestCase {

Now, the only GameListController that the test have access to is the same one the storyboard is instantiating, and I am finally able to type cast it. Here is the new testcase:

import XCTest
import UIKit
import mytabletop

class GameListControllerTest: XCTestCase {
    let sut: GameListController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("GameListController") as GameListController

    override func setUp() {
        super.setUp()
        UIApplication.sharedApplication().keyWindow.rootViewController = sut
        XCTAssertNotNil(sut.view)
    }

    func testInstance() {
        XCTAssertNotNil(sut)
        XCTAssertNotNil(sut.tableView) // UITableViewController property
        XCTAssertNotNil(sut.store) // instance property
        XCTAssertNotNil(sut.someButton) // outlet
    }
}

Now I am able to typecast correctly (scroll to see as GameListController) during the instance initialization. In order to force all outlets to be bound correctly and subviews to render accordingly to the device the test is being run, we can make the view controller the rootViewController for the app and pull the view from it, as seen in the setUp function above. Even myCustomOutlet is now working correctly.

like image 65
Irae Carvalho Avatar answered Jan 07 '23 15:01

Irae Carvalho