如何测试对象deinit时移除基于Block的NotificationCenter观察者?
问题:Block方式注册的通知观察者无法在单元测试中验证移除成功
我通过Block方式给Foo类注册了通知观察者,并在deinit方法中调用removeObserver尝试移除观察者,但单元测试始终失败,无法验证实例释放时观察者已被正确移除。
现有代码
Foo类实现
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 } } // 扩展通知名称 extension Notification.Name { static let Foo = Notification.Name("Foo") }
Mock 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) } }
单元测试代码
func testDeinit_DoesRemoveObserver() { // Given var sut: Foo? let mockNC = MockNotificationCenter() // When sut = Foo(nc: mockNC) // Then XCTAssertEqual(mockNC.notifications.count, 1) // 断言成功 // When sut = nil // Then XCTAssertEqual(mockNC.notifications.count, 0) // 断言失败 }
测试执行后报错:XCTAssertEqual failed: ("1") is not equal to ("0")
问题根源
使用addObserver(forName:object:queue:using:)注册观察者时,返回的是一个独立的NSObjectProtocol类型观察者对象,不是调用这个方法的self。你在deinit中调用nc.removeObserver(self, ...)时,通知中心找不到对应观察者,自然无法完成移除,这就是测试失败的核心原因。
同时,原Mock的逻辑也有问题:它只是简单记录通知名称,没有关联对应的观察者对象,移除时的过滤逻辑完全无法准确反映真实的观察者状态。
修正方案
1. 修改Foo类,保存正确的观察者对象
在Foo中存储addObserver返回的观察者实例,移除时使用这个实例而不是self:
class Foo { private let nc: NotificationCenter // 保存Block方式返回的观察者对象 private var observer: NSObjectProtocol? init(nc: NotificationCenter = .default) { self.nc = nc observer = nc.addObserver(forName: .Foo, object: nil, queue: .main) { _ in Task { [weak self] in self?.doSomething() } } } deinit { // 使用保存的观察者实例执行移除 if let observer = observer { nc.removeObserver(observer) } } @objc private func doSomething() async { // triggers some async code } }
2. 修正MockNotificationCenter的逻辑
改为用字典记录每个观察者和对应的通知名称,移除时通过观察者对象精准删除记录:
class MockNotificationCenter: NotificationCenter { // 用字典关联观察者和通知名称 private var observerToNameMap: [NSObjectProtocol: NSNotification.Name?] = [:] // 对外提供当前注册的通知名称列表 var registeredNotificationNames: [NSNotification.Name?] { Array(observerToNameMap.values) } override func addObserver( forName name: NSNotification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol { let observer = super.addObserver(forName: name, object: obj, queue: queue, using: block) observerToNameMap[observer] = name return observer } override func removeObserver(_ observer: NSObjectProtocol) { observerToNameMap.removeValue(forKey: observer) super.removeObserver(observer) } override func removeObserver( _ observer: Any, name aName: NSNotification.Name?, object anObject: Any? ) { if let observer = observer as? NSObjectProtocol { observerToNameMap.removeValue(forKey: observer) } super.removeObserver(observer, name: aName, object: anObject) } }
3. 更新单元测试
使用Mock中新的registeredNotificationNames属性来断言:
func testDeinit_DoesRemoveObserver() { // Given var sut: Foo? let mockNC = MockNotificationCenter() // When sut = Foo(nc: mockNC) // Then XCTAssertEqual(mockNC.registeredNotificationNames.count, 1) // When sut = nil // Then XCTAssertEqual(mockNC.registeredNotificationNames.count, 0) }
内容的提问来源于stack exchange,提问作者Aaron
相关产品推荐
相关产品推荐

