Testing for UINavigationController deallocation is failing?

138 views Asked by At

I have the following unit test:

func testReferences() throws {
        var strongVC: UIViewController? = UIViewController()
        var strongNC: UINavigationController? = UINavigationController(rootViewController: strongVC!)
        weak var weakVC = strongVC
        weak var weakNC = strongNC
        strongVC = nil
        
        XCTAssertNotNil(weakVC)
        XCTAssertNotNil(weakNC)
        
        strongNC = nil
        
        XCTAssertNil(weakVC) // fails
        XCTAssertNil(weakNC) // fails
    }

The last two assertions are failing. Any way to reliably test for deallocation of UIViewController and UINavigationController?

1

There are 1 answers

3
Duncan C On BEST ANSWER

As I recall, the docs say that objects held weakly "may be released at any time." The operative part is "may be".

I'm guessing that your view controller and navigation controller are auto-released. This is a term that harks back to the days of manual reference counting, but is still relevant under the covers. The object is created with a retain count of 1, and then added to an "auto-release pool". Then, next time your app's current function returns and it visits the event loop, the "auto-release pool is drained", which means that every entry in the auto-release pool gets a release message, dropping it's retain count by 1. When the retain count drops to zero, the object is deallocated.

(ARC actually uses reference counting under the covers, and so retain counts and auto-release pools are still relevant. It's just that with ARC the compiler takes care of maintaining them for you. Your strong and weak references get turned into to calls to the low level system retain, release, and auto-release functions.)

I'm not sure if it would work in the context of a test, but you might be able to use code like this:

func testReferences() throws {
        var strongVC: UIViewController? = UIViewController()
        var strongNC: UINavigationController? = UINavigationController(rootViewController: strongVC!)
        weak var weakVC = strongVC
        weak var weakNC = strongNC
        strongVC = nil
        
        XCTAssertNotNil(weakVC)
        XCTAssertNotNil(weakNC)
        
        strongNC = nil
        DispatchQueue.main.async {
            XCTAssertNil(weakVC) // fails
            XCTAssertNil(weakNC) // fails
        }
    }
}

That code would cause the calls to XCTAssertNil() to be deferred until the next pass through the event loop.

The problem with that code is that by the time the call to DispatchQueue.main.async() is executed, the test may be over.

Edit:

As pointed out by Cristik in their comment, the better way would be to use an autoreleasepool command:

func testReferences() throws {
    //Define the vars we want to test outside of the auto-release pool statement.
    weak var weakVC: UIViewController
    weak var weakNC: UINavigationController
    autoreleasepool {
        var strongVC: UIViewController? = UIViewController()
        var strongNC: UINavigationController? = UINavigationController(rootViewController: strongVC!)
        weakVC = strongVC
        weakNC = strongNC
        strongVC = nil
        
        XCTAssertNotNil(weakVC)
        XCTAssertNotNil(weakNC)
        
        strongNC = nil
    }
    //Test for nil outside of the autorelasepool statement, 
    //after the auto-release pool is drained.
    XCTAssertNil(weakVC) // fails
    XCTAssertNil(weakNC) // fails
}