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
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:
we pass a closure that builds the
object
that we want to make sure is not leakingwe create the
object
inside anautoreleasepool
we keep a
weak
reference to theobject
outside of the poolbefore exiting the
autoreleasepool
, we assert that theobject
is indeed in memorywe exit the
autoreleasepool
, which should deallocate all the instances created within its scopefinally, we assert that after sometime the
weak
reference has indeed becomenil
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!
❤️