如何为此 func loadDemos() 编写单元测试?
这是代码
class BenefitViewModel: ObservableObject {
func loadDemos() {
let testMode = ProcessInfo.processInfo.arguments.contains("testMode")
if testMode {
self.demos = DummyData().decodeDemos()
} else {
cancellables.insert(self.getDemos().sink(receiveCompletion: { result in
switch result {
case .failure(let error):
print(error.localizedDescription)
break
case .finished:
break
}
}, receiveValue: { response in
self.demos = response
print(“Demos: \(response.count)")
}))
}
}
}
Run Code Online (Sandbox Code Playgroud)
冒着有点烦人的风险,我将回答您问题的更一般版本:如何对Combine 管道进行单元测试?
让我们退后一步,从一些关于单元测试的一般原则开始:
不要测试苹果的代码。你已经知道它的作用了。测试你的代码。
不要测试网络(除非在您只想确保网络正常运行的罕见测试中)。替换您自己的行为类似于网络的类。
异步代码需要异步测试。
我假设你getDemos做了一些异步网络。所以不失一般性,我可以用不同的管道来说明。让我们使用一个简单的 Combine 管道,它从网络获取图像 URL 并将其存储在 UIImage 实例属性中(这与您对管道响应和 执行的操作非常并行self.demos)。这是一个简单的实现(假设我有一些调用机制fetchImage):
class ViewController: UIViewController {
var image : UIImage?
var storage = Set<AnyCancellable>()
func fetchImage() {
let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
self.getImageNaive(url:url)
}
func getImageNaive(url:URL) {
URLSession.shared.dataTaskPublisher(for: url)
.compactMap { UIImage(data:$0.data) }
.receive(on: DispatchQueue.main)
.sink { completion in
print(completion)
} receiveValue: { [weak self] image in
print(image)
self?.image = image
}
.store(in: &self.storage)
}
}
Run Code Online (Sandbox Code Playgroud)
一切都很好,而且工作正常,但无法测试。原因是如果我们简单地调用getImageNaive我们的测试,我们将测试网络,这是不必要的和错误的。
所以让我们来测试一下。如何?好吧,在这个简单的例子中,我们只需要将异步发布者与管道的其余部分分开,这样测试就可以替代它自己的不进行任何网络连接的发布者。因此,例如(再次假设我有一些调用机制fetchImage):
class ViewController: UIViewController {
// Output is (data: Data, response: URLResponse)
// Failure is URLError
typealias DTP = AnyPublisher <
URLSession.DataTaskPublisher.Output,
URLSession.DataTaskPublisher.Failure
>
var image : UIImage?
var storage = Set<AnyCancellable>()
func fetchImage() {
let url = URL(string:"https://www.apeth.com/pep/manny.jpg")!
self.getImage(url:url)
}
func getImage(url:URL) {
let pub = self.dataTaskPublisher(for: url)
self.createPipelineFromPublisher(pub: pub)
}
func dataTaskPublisher(for url: URL) -> DTP {
URLSession.shared.dataTaskPublisher(for: url).eraseToAnyPublisher()
}
func createPipelineFromPublisher(pub: DTP) {
pub
.compactMap { UIImage(data:$0.data) }
.receive(on: DispatchQueue.main)
.sink { completion in
print(completion)
} receiveValue: { [weak self] image in
print(image)
self?.image = image
}
.store(in: &self.storage)
}
}
Run Code Online (Sandbox Code Playgroud)
你看出区别了吗?它几乎相同,但管道本身现在与发布者不同。我们的方法createPipelineFromPublisher将任何正确类型的发布者作为其参数。这意味着我们已经抽象出了 的使用URLSession.shared.dataTaskPublisher,并且可以替代我们自己的发布者。换句话说,createPipelineFromPublisher是可测试的!
好的,让我们来写测试。我的测试用例包含一个生成“模拟”发布者的方法,该方法只是发布一些数据,这些数据包装在与数据任务发布者相同的发布者类型中:
func dataTaskPublisherMock(data: Data) -> ViewController.DTP {
let fakeResult = (data, URLResponse())
let j = Just<URLSession.DataTaskPublisher.Output>(fakeResult)
.setFailureType(to: URLSession.DataTaskPublisher.Failure.self)
return j.eraseToAnyPublisher()
}
Run Code Online (Sandbox Code Playgroud)
我的测试包(称为 CombineTestingTests)还有一个资产目录,其中包含一个名为mannyTesting. 所以我所要做的就是createPipelineFromPublisher使用来自 UIImage 的数据调用 ViewController 的,并检查 ViewController 的image属性现在是否是相同的图像,对吗?
func testImagePipeline() throws {
let vc = ViewController()
let mannyTesting = UIImage(named: "mannyTesting", in: Bundle(for: CombineTestingTests.self), compatibleWith: nil)!
let data = mannyTesting.pngData()!
let pub = dataTaskPublisherMock(data: data)
vc.createPipelineFromPublisher(pub: pub)
let image = try XCTUnwrap(vc.image, "The image is nil")
XCTAssertEqual(data, image.pngData()!, "The image is the wrong image")
}
Run Code Online (Sandbox Code Playgroud)
错误的!测试失败;vc.image是nil。什么地方出了错?答案是,Combine 管道,即使是以 Just 开头的管道,都是异步的。异步管道需要异步测试。我的测试需要等待,直到vc.image是不是 nil。做到这一点的一种方法是使用谓词监视vc.image不再是nil:
func testImagePipeline() throws {
let vc = ViewController()
let mannyTesting = UIImage(named: "mannyTesting", in: Bundle(for: CombineTestingTests.self), compatibleWith: nil)!
let data = mannyTesting.pngData()!
let pub = dataTaskPublisherMock(data: data)
vc.createPipelineFromPublisher(pub: pub)
let pred = NSPredicate { vc, _ in (vc as? ViewController)?.image != nil }
let expectation = XCTNSPredicateExpectation(predicate: pred, object: vc)
self.wait(for: [expectation], timeout: 10)
let image = try XCTUnwrap(vc.image, "The image is nil")
XCTAssertEqual(data, image.pngData()!, "The image is the wrong image")
}
Run Code Online (Sandbox Code Playgroud)
并且测试通过!你明白重点了吗?这里的被测系统是完全正确的,即形成管道的机制,该管道接收数据任务发布者将发出的输出并设置我们的视图控制器的实例属性。我们已经测试了我们的代码,并且只测试了我们的代码。我们已经证明我们的管道工作正常。