How to mock URLSession.DataTaskPublisher

喜欢而已 提交于 2020-06-27 12:06:16

问题


How can I mock URLSession.DataTaskPublisher? I have a class Proxy that require to inject a 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)
    }

}

This code was originally used with the traditional version of URLSession with the completion handler. It was perfect since I could easily mock URLSession for testing like Sundell's solution here: Mocking in Swift.

Is it possible to do the same with the Combine Framework?


回答1:


In the same way that you can inject a URLSessionProtocol to mock a concrete session, you can also inject a mocked Publisher. For example:

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

However, depending on what you do with this publisher you might have to address some weirdnesses with Combine async publishers, see this post for additional discussion:

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




回答2:


Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...

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)
    }
  }
}

Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:

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

Then in your network layer:

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

Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given 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: $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 }
  }

The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.

(Note: I'm using Mockingbird for mocks).




回答3:


The best way to test your client is to use URLProtocol.

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

You can intercept all your request before she performs the real request on the cloud, and so creates your expectation. Once you have done your expectation, she will be destroyed, so you never make real requests. Tests more reliable, faster, and you got the control!

You got a little example here: https://www.hackingwithswift.com/articles/153/how-to-test-ios-networking-code-the-easy-way

But it's more powerful than just this, you can do everything you want like: Check your Events/Analytics...

I hope it'll help you!



来源:https://stackoverflow.com/questions/59521928/how-to-mock-urlsession-datataskpublisher

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!