依赖Publisher的ViewModel单元测试:receiveCompletion未触发问题
问题分析与解决
你实现了一个Service类,通过loadModels方法返回Publisher加载数据:
class Service { let fileURL: URL // 在程序其他地方设置 func loadModels() -> AnyPublisher<[MyModelClass], Error> { URLSession.shared.dataTaskPublisher(for: fileURL) .map( { $0.data } ) .decode(type: [MyModelClass].self, decoder: JSONDecoder()) .receive(on: DispatchQueue.main) .eraseToAnyPublisher() } }
ViewModel调用该方法,通过@Published属性models接收数据:
class ViewModel: ObservableObject { @Published var models: [MyModelClass]? var cancellables = Set<AnyCancellable>() let service: Service init(service: Service) { self.service = service loadCityData() // 注:此处疑似笔误,应为loadModels() } func loadModels() { service.loadModels() .sink { _ in } receiveValue: { [weak self] models in self?.models = models } .store(in: &cancellables) } }
单元测试时,你监听ViewModel的$models,发现receiveCompletion从未触发,但receiveValue会多次调用:
let expectation = expectation(description: "loadModels") viewModel.$models .receive(on: RunLoop.main) .sink(receiveCompletion: { _ in expectation.fulfill() }, receiveValue: { _ in }) .store(in: &cancellables) // 类级别的属性 viewModel.loadModels() wait(for: [expectation], timeout: 10)
而直接监听Service返回的Publisher时,receiveCompletion能正常触发,核心原因如下:
核心差异:两种Publisher的生命周期不同
- Service返回的是有限Publisher:
dataTaskPublisher是一次性任务,请求完成(成功/失败)后会立即发送completion事件并终止,所以监听它的sink能收到completion回调。 - @Published生成的是无限Publisher:
$models是长期存在的Publisher,只要ViewModel对象存活,它就会在models值变化时持续发送新值,永远不会主动发送completion事件——这就是测试中receiveCompletion永远不触发的根本原因。
测试的正确写法
如果要通过监听$models验证加载完成,可以通过过滤值+取首次有效结果来触发预期:
let expectation = expectation(description: "loadModels") viewModel.$models .receive(on: RunLoop.main) .compactMap { $0 } // 过滤nil值,只保留有效数据 .first() // 只取第一个非空结果 .sink(receiveValue: { _ in expectation.fulfill() }) .store(in: &cancellables) viewModel.loadModels() wait(for: [expectation], timeout: 10)
也可以在ViewModel中新增状态属性(比如@Published var isLoading = false),在loadModels的sink回调中更新状态,测试时监听该状态的变化。
内容的提问来源于stack exchange,提问作者Ramy Al Zuhouri
相关产品推荐
相关产品推荐

