Swift XCTest: Verify proper deallocation of weak variables

2.2k views Asked by At

Recently I was attempting to verify that an object I wrote properly deallocates using a unit test. I found however that no matter what I tried the object would not deallocate before the test completed. So I reduced the test to a trivial example (seen below) that attempts to prove the basics of object deallocation using weak variables.

In my mind, the strong reference should stop retaining the object after the test method exits, and the weak reference should be nil when referenced on the next run loop. However, the weak reference is never nil and both tests fail. Am I misunderstanding something here? Below are the unit tests in full.

class Mock { //class type, should behave with reference semantics

    init() { }
}

class DeallocationTests: XCTestCase {   

    func testWeakVarDeallocation() {   

        let strongMock = Mock()

        weak var weakMock: Mock? = strongMock

        let expt = expectation(description: "deallocated")

        DispatchQueue.main.async {

            XCTAssertNil(weakMock)      //This assertion fails

            expt.fulfill()
        }

        waitForExpectations(timeout: 1.0, handler: nil)
    }

    func testCaptureListDeallocation() {   

        let strongMock = Mock()

        let expt = expectation(description: "deallocated")

        DispatchQueue.main.async { [weak weakMock = strongMock] in

            XCTAssertNil(weakMock)      //This assertion also fails

            expt.fulfill()
        }

        waitForExpectations(timeout: 1.0, handler: nil)
    }
}

I thought that maybe deallocation was being deferred somehow by XCTest, but even wrapping the test method bodies in an autoreleasepool did not cause the object to deallocate.

1

There are 1 answers

4
par On BEST ANSWER

The problem is that your testWeakVarDeallocation() function hasn't exited when the dispatchAsync block is called so a strong reference to strongMock is still held.

Try it like this (allowing testWeakVarDeallocation() to exit) and you'll see weakMock becomes nil as expected:

class weakTestTests: XCTestCase {
    var strongMock: Mock? = Mock()

    func testWeakVarDeallocation() {
        weak var weakMock = strongMock

        print("weakMock is \(weakMock)")

        let expt = self.expectation(description: "deallocated")

        strongMock = nil

        print("weakMock is now \(weakMock)")

        DispatchQueue.main.async {
            XCTAssertNil(weakMock)      // This assertion fails

            print("fulfilling expectation")
            expt.fulfill()
        }

        print("waiting for expectation")
        self.waitForExpectations(timeout: 1.0, handler: nil)
        print("expectation fulfilled")
    }
}