如何对可取消项进行单元测试?
Posted
技术标签:
【中文标题】如何对可取消项进行单元测试?【英文标题】:How To UnitTest Combine Cancellables? 【发布时间】:2021-02-03 22:04:09 【问题描述】:如何为这个函数 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)")
))
【问题讨论】:
你想测试什么? 我想确定Demos:response.count > 0 好吧,你只需模拟getDemos
并进行正常的异步测试。
@matt,getDemos() 返回此类型 AnyPublishergetDemos
实际上执行了一些网络操作。您总是需要模拟网络以进行测试;不小心测试真网是没有意义的!所以你会有一个 getDemos
的版本,它只返回一个 Just 或类似的,以便我们绕过网络。然后,正如我所说,您的代码仍然是异步的,因此您的测试将使用 Expectation。
【参考方案1】:
冒着有点烦人的风险,我将回答您的问题的更一般版本:您如何对组合管道进行单元测试?
让我们退后一步,从有关单元测试的一些一般原则开始:
不要测试 Apple 的代码。你已经知道它的作用了。测试你的代码。
不要测试网络(除非在您只想确保网络正常运行的罕见测试中)。替换您自己的行为类似于网络的类。
异步代码需要异步测试。
我假设你的 getDemos
做了一些异步网络。因此,在不失一般性的情况下,我可以用不同的管道来说明。让我们使用一个简单的组合管道,该管道从网络获取图像 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)
一切都很好,而且运行良好,但无法测试。原因是如果我们在测试中简单地调用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)
你看出区别了吗?几乎相同,但管道本身现在与发布者不同。我们的方法 createPipelineFromPublisher
将 any 正确类型的发布者作为其参数。这意味着我们已经抽象出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()
我的测试包(称为 CombineTestingTests)还有一个资产目录,其中包含一个名为 mannyTesting
的 UIImage。所以我所要做的就是使用来自该 UIImage 的数据调用 ViewController 的 createPipelineFromPublisher
,并检查 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")
错了!测试失败; 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")
测试通过了!你明白重点了吗?这里的被测系统完全正确,即形成管道的机制,该管道接收数据任务发布者将发出的输出并设置我们的视图控制器的实例属性。我们测试了我们的代码和仅我们的代码。我们已经证明我们的管道可以正常工作。
【讨论】:
哦,天哪,我刚看到这个!你成功了!我一直在为如何对组合管道进行单元测试而苦苦挣扎! @matt,我也会看看你的书!非常感谢您致力于帮助初学者! 您会建议您将哪本书重点放在结合主题上? apeth.com/UnderstandingCombine 但它没有谈论测试。也许我应该添加一个关于这个的部分,不确定。 在func dataTaskPublisher(for url: URL) -> DTP
中缺少return
吗?
@Laufwunder 不,除非你使用的是非常旧的 Swift 版本。【参考方案2】:
在@Joakim Danielson 的帮助下解决这个问题How to convert myObject to AnyPublisher<myObject, Never>?。我想出了这个答案。
func testDemoData()
let testDemoData = Just(demoData).eraseToAnyPublisher()
cancellables.insert(testDemoData.sink(receiveCompletion: [weak self] result in
switch result
case .failure(let error):
XCTAssert(true)
print(error)
break
case .finished:
break
, receiveValue: [weak self] response in
XCTAssert((response!.count ) >= 0)
))
【讨论】:
这很可爱,但它并没有测试你说要测试的内容,即loadDemos
中的响应计数。这可能是一个微不足道的测试,在任何时候都不会接触你的真实代码。
哈哈..你能分享一个答案吗?
我的评论告诉你我的建议。
@matt 你能分享答案吗?或者更正/编辑我的答案?我真的不明白你的建议。以上是关于如何对可取消项进行单元测试?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Service 构造函数中对 Controller 进行单元测试和模拟 @InjectModel