如何模拟 URLSession.DataTaskPublisher

How to mock URLSession.DataTaskPublisher

如何模拟 URLSession.DataTaskPublisher?我有一个 class 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.

是否可以用 Combine Framework 做同样的事情?

就像您可以注入 URLSessionProtocol 来模拟具体会话一样,您也可以注入模拟的 Publisher。例如:

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

但是,根据您对该发布者所做的操作,您可能必须解决 Combine 异步发布者的一些奇怪问题,请参阅此 post 以获取更多讨论:

因为 DataTaskPublisher 使用 URLSession 创建它,你可以模拟它。我最终创建了一个 URLSession 子类,覆盖了 dataTask(...) 到 return 一个 URLSessionDataTask 子类,我用我需要的 data/response/error 喂养了它...

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: [=12=])
          .mapError { APIError.urlError([=12=])} } }
      .eraseToAnyPublisher()
  }

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

  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: [=13=])
    }
    let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
    let publisher: AnyPublisher<TestCodable, APIError> = 
    api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
    let _ = publisher.sink(receiveCompletion: {
      switch [=13=] {
      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 进行模拟)。

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

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

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

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

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

希望对您有所帮助!