How to write tests that detect memory leaks 💦

Hi 👋

Before we go into the topic of this email, I have a big thank you to my sponsor for another week: Proxyman 🧑‍🚀


Advertisement

Need to debug HTTP/HTTPS network on iOS apps?

Try Proxyman, a native macOS app that captures and displays Request/Response in beautiful UIs.

Supports iOS device and Simulator.

👉 Download macOS app now! 👈


Sponsors like Proxyman really help me grow my content creation, so if you have time please make sure to check out this product: it’s a direct support to my content creation ☺️


Memory leaks are probably one of the most frustrating kind of bugs in iOS apps: they can cause a lot of damage and they are often very hard to fix.

So whenever we solve a memory issue, we really want to make sure that it won’t happen again in the future!

That’s why in this email I want to show you a nice little trick that will enable you to write unit tests that can detect a memory leak!

And as you will see, this goal is actually easier to achieve than it seems!

First, we’re going to create an extension of XCTestCase and declare a new method assertDeallocatation():

There’s a bit of code in this method, but the logic is quite straightforward:

  1. we pass a closure that builds the object that we want to make sure is not leaking

  2. we create the object inside an autoreleasepool

  3. we keep a weak reference to the object outside of the pool

  4. before exiting the autoreleasepool, we assert that the object is indeed in memory

  5. we exit the autoreleasepool, which should deallocate all the instances created within its scope

  6. finally, we assert that after sometime the weak reference has indeed become nil

And if after a few seconds the weak reference hasn’t become nil, then it means that the instance is probably leaking and so we make the test fail.

So let’s give it a try!

Here’s a piece of code that contains a retain cycle:

If we use our method assertDeallocatation() with this code, you can see that the memory leak has indeed been successfully detected 🙌

And now, if we fix our code to remove the retain cycle…

…and run the test one more time…

…we can see that this time the test succeeds, because there’s no memory leak anymore ✌️

If you’re curious to see a full demo, you can watch this video I made last year:

Here’s the full code if you want to experiment with it:

import XCTest

extension XCTestCase {

    func assertDeallocation<T: AnyObject>(
        of object: () -> T,
        file: StaticString = #file,
        line: UInt = #line
    ) {
        weak var weakReferenceToObject: T?

        let autoreleasepoolExpectation = expectation(description: "Autoreleasepool should drain")

        autoreleasepool {
            let object = object()

            weakReferenceToObject = object

            XCTAssertNotNil(weakReferenceToObject)

            autoreleasepoolExpectation.fulfill()
        }

        wait(for: [autoreleasepoolExpectation], timeout: 10.0)

        wait(
            for: weakReferenceToObject == nil,
            timeout: 3.0,
            description: "The object should be deallocated since no strong reference points to it.",
            file: file,
            line: line
        )
    }

    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
        )
    }
}



class A {
    var b: B?

    init() { self.b = B(a: self) }
}

class B {
    var a: A

    init(a: A) { self.a = a }
}

class AssertDeallocatedTests: XCTestCase {

    func testAssertDeallocated() {
        assertDeallocation {
            return A()
        }
    }
}

That’s all for this email, thanks for reading it!

If you’ve enjoyed it, feel free to forward it
to your friends and colleagues 🙌

I wish you an amazing week!

❤️

Previous
Previous

Hidden feature: final

Next
Next

Bad practice: not using multiline strings