KISS πŸ‡ΊπŸ‡¦

Stop the war!

Stop the war in Ukraine! Fuck putin!

More information is at: https://war.ukraine.ua/.

There is a fund to support the Ukrainian Army: https://savelife.in.ua/en/donate/, and there is a special bank account that accepts funds in multiple currencies: https://bank.gov.ua/en/about/support-the-armed-forces. I donated to them. Please donate if you can!

Killer putin

Killer putin. Source: politico.eu.

Arrested putin

"It hasn't happened yet, but it will happen sooner or later. Beautiful photo, isn't it?" Source: twitter.

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

Comments