Memory leaks prevention using an autoreleasepool in unit tests

Memory leaks often happen without notice. Although best practices like using a weak reference to self inside closures help a lot, it’s often not enough to prevent you from finding a certain memory peak during the development of a project.

We can use memory graph debugging or Xcode Instruments to help find and fix leaked and abandoned memory. This, however, requires us to make it part of our regular workflow to prevent memory leaks from sneaking in.

Making it easy to test your app for memory leaks prevent us from introducing them without notice. This will not avoid all leaks, but it will decrease the chance at least.

Avoiding memory leaks using unit tests

Writing a unit test by combining a weak reference with an autoreleasepool makes it easy to verify deallocation. This can be seen as verifying that the deinit of a class is called and the memory is released.

In the following example, we’re verifying that a view controller is released. By creating an easy to use extension method on XCTestCase you make it easy to add this method to any of your view controller unit test classes. Besides, it’s a great way to verify that your view controllers get released correctly.

/// Ensures that the OwnedBucketViewController gets deallocated after being added to the navigation stack, then popped.
func testDeallocation() {
    assertDeallocation { () -> UIViewController in
        let bucket = Bucket()
        let viewModel = OwnedBucketViewModel(bucket: bucket)
        return OwnedBucketViewController(viewModel: viewModel)
    }
}

The extension creates a weak reference to the view controller which is created in the closure. After that, we present and dismiss the created view controller after which we’re verifying that the weak reference is set to nil.

extension XCTestCase {

    /// Verifies whether the given constructed UIViewController gets deallocated after being presented and dismissed.
    ///
    /// - Parameter testingViewController: The view controller constructor to use for creating the view controller.
    func assertDeallocation(of testedViewController: () -> UIViewController) {
        weak var weakReferenceViewController: UIViewController?

        let autoreleasepoolExpectation = expectation(description: "Autoreleasepool should drain")
        autoreleasepool {
            let rootViewController = UIViewController()

            // Make sure that the view is active and we can use it for presenting views.
            let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
            window.rootViewController = rootViewController
            window.makeKeyAndVisible()

            /// Present and dismiss the view after which the view controller should be released.
            rootViewController.present(testedViewController(), animated: false, completion: {
                weakReferenceViewController = rootViewController.presentedViewController
                XCTAssertNotNil(weakReferenceViewController)

                rootViewController.dismiss(animated: false, completion: {
                    autoreleasepoolExpectation.fulfill()
                })
            })
        }
        wait(for: [autoreleasepoolExpectation], timeout: 10.0)
        wait(for: weakReferenceViewController == nil, timeout: 3.0, description: "The view controller should be deallocated since no strong reference points to it.")
    }

    /// Checks for the callback to be the expected value within the given timeout.
    ///
    /// - Parameters:
    ///   - condition: The condition to check for.
    ///   - timeout: The timeout in which the callback should return true.
    ///   - description: A string to display in the test log for this expectation, to help diagnose failures.
    func wait(for condition: @autoclosure @escaping () -> Bool, timeout: TimeInterval, description: String, file: StaticString = #file, line: UInt = #line) {
        let end = Date().addingTimeInterval(timeout)

        var value: Bool = false
        let closure: () -> Void = {
            value = condition()
        }

        while !value && 0 < end.timeIntervalSinceNow {
            if RunLoop.current.run(mode: RunLoop.Mode.default, before: Date(timeIntervalSinceNow: 0.002)) {
                Thread.sleep(forTimeInterval: 0.002)
            }
            closure()
        }

        closure()

        XCTAssertTrue(value, "➡️? Timed out waiting for condition to be true: \"\(description)\"", file: file, line: line)
    }
}

If any logic within the testing view controller retains the given view controller, this test would fail.

To validate that our weak reference is set to nil we’re using another extension method on XCTestCase which is a very handy method to periodically check for a certain condition to match.

The XCTest API brings a great API for creating expectations for notifications, predicates or key-value observation, but none of these make it possible to validate that the weak reference is eventually set to nil. Our new extension method does make this possible. It checks for the given amount of time whether the condition is matched and if not, it fails.

The use of an autoreleasepool

Without the autoreleasepool we would’ve not been able to verify that the weak reference would eventually be released. All the references within the autoreleasepool closure should be released when it’s drained. This only happens if no strong references exist.

The above view controller example can be used to build more unit tests to verify the deallocation of your objects.

More on memory debugging

WWDC over the years dedicated quite a few sessions to memory debugging. For example, the iOS Memory Deep Dive session of WWDC 2018 which helps you understand the true memory cost of an image and gives you tips and tricks for reducing the memory footprint of an app.

Collect by WeTransfer Collect by WeTransfer is the best way to organize your ideas. Save content from across your apps and bring it together for your friends, your team, or just for yourself.