Say I have this code:
class Foo {
private let nc: NotificationCenter
init(nc: NotificationCenter = .default) {
self.nc = nc
nc.addObserver(forName: Notification.Name.Foo, object: nil, queue: .main) { _ in
Task { [weak self] in
self?.doSomething()
}
}
}
deinit {
nc.removeObserver(self, name: Notification.Name.Foo, object: nil)
}
@objc
private func doSomething() async {
// triggers some async code
}
}
I want to write a unit test to ensure this observer is removed when the class is deallocated.
The Apple docs specify:
If you used addObserver(forName:object:queue:using:) to create your observer, you should call this method or removeObserver(_:name:object:) before the system deallocates any object that addObserver(forName:object:queue:using:) specifies.
I've mocked a NotificationCenter:
class MockNotificationCenter: NotificationCenter {
var notifications: [NSNotification.Name?] = []
override func addObserver(
forName name: NSNotification.Name?,
object obj: Any?,
queue: OperationQueue?,
using block: @escaping (Notification) -> Void) -> NSObjectProtocol
{
notifications.append(name)
return super.addObserver(forName: name, object: obj, queue: queue, using: block)
}
override func removeObserver(
_ observer: Any,
name aName: NSNotification.Name?,
object anObject: Any?
) {
notifications = notifications.filter { $0 != aName }
super.removeObserver(observer, name: aName, object: anObject)
}
And here's my unit test:
func testDeinit_DoesRemoveObserver() {
// Given
var sut: Foo?
let mockNC = MockNotificationCenter()
// When
sut = Foo(notificationCenter: mockNC)
// Then
XCTAssertEqual(mockNC.notifications.count, 1) // Succeeds
// When
sut = nil
// Then
XCTAssertEqual(mockNC.notifications.count, 0) // Fails
}
XCTAssertEqual failed: ("1") is not equal to ("0")
How would I fix this to assert that an observer has been removed from the notification center upon deallocation of my observing object?
The problem is there is a retain cycle in your Foo
class. In the addObserver
closure, you're capturing the Foo class instance itself. It should be:
nc.addObserver(forName: Notification.Name.Foo, object: nil, queue: .main) { [weak self] _ in
Task {
self?.doSomething()
}
}
At the time sut
was assigned to nil, the deinit
and also removeObserver
didn't get called, and the notification array still remains one element.