如何模拟 URLSession.DataTaskPublisher

Posted

技术标签:

【中文标题】如何模拟 URLSession.DataTaskPublisher【英文标题】:How to mock URLSession.DataTaskPublisher 【发布时间】:2020-04-18 16:10:41 【问题描述】:

如何模拟URLSession.DataTaskPublisher?我有一个类 Proxy 需要注入 URLSessionProtocol

protocol URLSessionProtocol 
    func loadData(from url: URL) -> URLSession.DataTaskPublisher

class Proxy 

    private let urlSession: URLSessionProtocol

    init(urlSession: URLSessionProtocol) 
        self.urlSession = urlSession
    

    func get(url: URL) -> AnyPublisher<Data, ProxyError> 
        // Using urlSession.loadData(from: url)
    



此代码最初与带有完成处理程序的传统版本的URLSession 一起使用。这是完美的,因为我可以轻松地模拟 URLSession 来进行像 Sundell 的解决方案一样的测试:Mocking in Swift。

是否可以对组合框架做同样的事情?

【问题讨论】:

【参考方案1】:

与您可以注入URLSessionProtocol 来模拟具体会话的方式相同,您也可以注入模拟的Publisher。例如:

let mockPublisher = Just(MockData()).eraseToAnyPublisher()

但是,根据您对这个发布者所做的事情,您可能需要解决组合异步发布者的一些奇怪问题,请参阅这篇文章进行更多讨论:

Why does Combine's receive(on:) operator swallow errors?

【讨论】:

我理解这个概念,但是您将如何实现呢?我正在努力在我的模拟 loadData 函数中返回一个 URLSession.DataTaskPublisher 。您能否提供有关此解决方案的更多详细信息。【参考方案2】:

由于DataTaskPublisher 使用URLSession 创建它,你可以模拟它。我最终创建了一个URLSession 子类,覆盖dataTask(...) 以返回一个URLSessionDataTask 子类,我提供了我需要的数据/响应/错误...

class URLSessionDataTaskMock: URLSessionDataTask 
  private let closure: () -> Void

  init(closure: @escaping () -> Void) 
    self.closure = closure
  

  override func resume() 
    closure()
  


class URLSessionMock: URLSession 
  var data: Data?
  var response: URLResponse?
  var error: Error?

  override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask 
    let data = self.data
    let response = self.response
    let error = self.error
    return URLSessionDataTaskMock 
      completionHandler(data, response, error)
    
  

那么显然你只希望你的网络层使用这个URLSession,我去工厂做这个:

protocol DataTaskPublisherFactory 
  func make(for request: URLRequest) -> URLSession.DataTaskPublisher

然后在你的网络层:

  func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable 
    Just(request)
      .flatMap  
        self.dataTaskPublisherFactory.make(for: $0)
          .mapError  APIError.urlError($0)  
      .eraseToAnyPublisher()
  

现在您可以使用 URLSession 子类在测试中传递一个模拟工厂(这个断言 URLErrors 映射到自定义错误,但您也可以断言给定数据/响应的其他一些条件):

  func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() 
    let session = URLSessionMock()
    session.error = TestError.test
    let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
    given(dataTaskPublisherFactory.make(for: any())) ~> 
      session.dataTaskPublisher(for: $0)
    
    let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
    let publisher: AnyPublisher<TestCodable, APIError> = 
    api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
    let _ = publisher.sink(receiveCompletion: 
      switch $0 
      case .failure(let error):
        XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
      case .finished:
        XCTFail()
      
    )  _ in 
  

其中一个问题是 URLSession init()ios 13 中已被弃用,因此您必须在测试中接受警告。如果有人能找到解决办法,我将不胜感激。

(注意:我使用 Mockingbird 进行模拟)。

【讨论】:

iOS13 弃用了正常的 DataTask init(),这会导致您的子类模拟 'init()' was deprecated in iOS 13.0: Please use -[NSURLSession dataTaskWithRequest:] or other NSURLSession methods to create instances 发出警告【参考方案3】:

测试客户端的最佳方法是使用URLProtocol

https://developer.apple.com/documentation/foundation/urlprotocol

您可以在她在云端执行真正的请求之前拦截您的所有请求,从而创造您的期望。一旦你完成了你的期望,她就会被摧毁,所以你永远不会提出真正的要求。 测试更可靠、更快速,您可以掌控一切!

这里有一个小例子:https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way

但它比这更强大,你可以做任何你想做的事情:检查你的事件/分析......

希望对你有帮助!

【讨论】:

以上是关于如何模拟 URLSession.DataTaskPublisher的主要内容,如果未能解决你的问题,请参考以下文章

如何从模拟路径加载模拟文件

如何在WPF中模拟鼠标点击

如何在安卓模拟器中模拟加速度计? [关闭]

数字信号和模拟信号之间如何让相互转换?

你如何在 GoLang 的模拟中模拟错误返回值?

如何判断是不是被检测到使用模拟器了