You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试对象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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 02:53:21