@Published 未在测试中发布更新

@Published not emitting update in tests

所以我遇到了这个问题,@Published 领域结果在测试中没有更新。

基本上我想做的是测试领域更新是否导致我的 ViewModel 更新 ui。为此,我订阅了已发布的字段。

这是测试:

   func testListUpdate() {
       // when
       let expectation = XCTestExpectation(description: "Wasn't updated.")

       _ = sut.$stocks
           .dropFirst()
           .sink { value in
               expectation.fulfill()
           }

       try! realm.write {
           realm.add(Stock(value: ["name": "Test3"]))
       }

       // then
       wait(for: [expectation], timeout: 1)
   }

sut 是如下所示的 ViewModel:

import Foundation
import RealmSwift

class StockListViewModel: ViewModelBase, ObservableObject {
    let stockRepository: StockRepository

    @Published var stocks: Results<Stock>

    init(stockRepository: StockRepository) {
        self.stockRepository = stockRepository

        stocks = stockRepository.all
            .sorted(byKeyPath: "createdAt", ascending: true)

        super.init(title: "StockList_title".localize())

        self.notificationTokens.append(
            stocks
                .observe({ change in
                    switch change {
                    case .update(let updated, _, _, _):
                        self.stocks = updated
                    default: break
                    }
                })
        )
    }
}

如您所见,视图模型具有 `@Published var stocks: Results.

在初始化程序中设置了库存(此更新由 Publisher 成功发出并在另一种情况下进行了测试。

然后我为这些股票分配了一个领域更改侦听器,当它更新时,我将这些股票设置为新的(参见 observe 闭包)。确实调用了此更新,但测试中的 sink 从未发出更新。因此测试失败。

所以我的问题是:为什么 Publisher 发出初始值,但从来没有发出更新值?

问题是 Publisher 的订阅没有保存。

所以行

       _ = sut.$stocks

应该是

       cancellable = sut.$stocks

其中 cancellable 是测试类的一个字段。

使用 Combine 时,您始终需要保留对代表订阅的 AnyCancellable 的引用 - 在您的测试用例中,return 由 sink 调用编辑,因为订阅链仅在 AnyCancellable 保存在内存中时保持活动状态。 Publishers 只有在有订阅者时才会发出值 - 所以如果你放弃订阅,你的 $stocks Publisher 将没有订阅者,因此它不会发出任何值。

您通过丢弃 sink 中的 return 值来丢弃订阅。

_ = sut.$stocks
       .dropFirst()
       .sink 

相反,您需要将 return 值存储在测试 class 上作为实例 属性。

final class YourTestClass: XCTestCase {

    var cancellable: AnyCancellable?    

    func testListUpdate() {
       // when
       let expectation = XCTestExpectation(description: "Wasn't updated.")

       cancellable = sut.$stocks
           .dropFirst()
           .sink { value in
               expectation.fulfill()
           }

       try! realm.write {
           realm.add(Stock(value: ["name": "Test3"]))
       }

       // then
       wait(for: [expectation], timeout: 1)
   }    
}