我实现了一个服务类,其中包含一个函数,当加载一些数据时,该函数返回一个发布者:
class Service {
let fileURL: URL // Set somewhere else in the program
func loadModels() -> AnyPublisher<[MyModelClass], Error> {
URLSession.shared.dataTaskPublisher(for: fileURL)
.map( { $0.data } )
.decode(type: [MyModelClass].self, decoder: JSONDecoder())
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
这个函数在我的视图模型类中是这样使用的:
class ViewModel: ObservableObject {
@Published var models: [MyModelClass]?
var cancellables = Set<AnyCancellable>()
let service: Service
init(service: Service) {
self.service = service
loadCityData()
}
func loadModels() {
service.loadModels()
.sink { _ in
} receiveValue: { [weak self] models in
self?.models = models
}
.store(in: &cancellables)
}
}
我发现视图模型很难进行单元测试,因为我没有直接从单元测试类中的服务返回的发布者,而是使用了@Published属性,所以我尝试实现了这样一个测试:
let expectation = expectation(description: "loadModels")
viewModel.$models
.receive(on: RunLoop.main)
.sink(receiveCompletion: { _ in
finishLoading.fulfill()
}, receiveValue: { _ in
})
.store(in: &cancellables) // class-scoped property
viewModel.loadModels()
wait(for: [expectation], timeout: 10)
问题是receiveComplection回调函数从来没有被调用过。如果发布者可用(从Service对象返回的那个),应用于发布者的相同代码将成功运行并实现预期。相反,没有调用complection,但receiveValue被多次调用。为什么?
2条答案
按热度按时间gc0ot86w1#
首先,不是将整个服务传递给视图模型,而是只传递发布者本身。在测试中,您可以传递一个同步发布者,这使得测试更加容易。
注意,不需要设置期望值并等待它,这使得测试更快。
更简单的方法是直接检查
models
属性:但是,对于所有这些,您认为您在这里测试的究竟是什么?在此代码中没有转换和逻辑。您没有进行测试以确保ViewModel调用到服务中,因为为了执行此测试,您必须模拟服务。因此,实际上,你所做的唯一的事情就是测试,看看测试本身是否正确地模拟了服务。但是这有什么意义呢?如果测试设置不正确,谁在乎呢?测试测试生产代码?
8cdiaqws2#
您可以使用接口和依赖注入的组合来进行测试。
首先定义服务的接口:
接下来,让
Service
符合这个新协议:现在可以使用上面定义的接口将Service注入到
ViewModel
中:这意味着您可以将任何符合
ServiceInterface
的实体注入到ViewModel
中,因此让我们在测试目标中定义一个实体:最后,让我们将
MockService
注入ViewModel
以进行测试: