KISS

Keep It Simple Stupid

Unit-testing absence of retain cycles in swift

| comments

ARC in Objective-C and swift is a nice technology, between manual memory management and a garbage collector. But it’s not fool-proof and you can still create retain cycles when two objects retain each other. There are numerous blog posts online explaining this problem.

It would be nice to have unit tests to verify your classes don’t have this issue, wouldn’t it? There’s a way to do that.

ReactiveSwift has the Lifetime object to track the deallocation of other objects and provide reactive properties for them. We can use it to write a unit-test to verify that an object is indeed deallocated at the end of the test and there are no internal retain cycles related to the object under test. I’ve used this approach in the project I’m working on to ensure new objects are deallocated after use.

I came up with this assertObjectIsDeallocated assertion that takes an optional object and its name and sets up the lifetime observation to wait until the object is deallocated:

1
2
3
4
5
6
7
8
9
10
11
12
func assertObjectIsDeallocated(_ object: AnyObject?, objectName: String) {
    object.map(Lifetime.of).map { (lifetime: Lifetime) in
        if !lifetime.hasEnded {
            let lifetimeEndedExpectation = expectation(description: "\(objectName) is deallocated")
            lifetime.observeEnded {
                lifetimeEndedExpectation.fulfill()
            }

            waitForExpectations(timeout: 1.0)
        }
    }
}

The sample project is available in this repository: https://github.com/eunikolsky/TestingRetainCycles. You can find a MainViewController that uses a Downloader and the view controller is the downloader’s delegate, which creates a possibility of a retain cycle if not done correctly. The test function itself only starts the download, but doesn’t assert anything; the deallocation assertion is automatically done after each test in tearDown:

1
2
3
4
5
6
override func tearDown() {
    weak var _sut = sut; sut = nil
    super.tearDown()

    assertObjectIsDeallocated(_sut, objectName: "MainViewController")
}

This commit has the retain cycle and the test fails:

1
TestingRetainCycles/TestingRetainCyclesTests/TestingRetainCyclesTests.swift:39: error: -[TestingRetainCyclesTests.TestingRetainCyclesTests testViewControllerShouldBeDeallocated] : Asynchronous wait failed: Exceeded timeout of 1 seconds, with unfulfilled expectations: "MainViewController is deallocated".

Next commit has the fix, which is making the Downloader’s delegate property weak and the test now passes:

1
Test Case '-[TestingRetainCyclesTests.TestingRetainCyclesTests testViewControllerShouldBeDeallocated]' passed (0.034 seconds).

swift

Note: The comments in the blog are provided by disqus.com; if you don't see the comment form under the post, probably your browser or its extension (such as uBlock Origin or NoScript) blocks their scripts.

« OSX: Close notification with keyboard

Comments