How to mock DataTaskPublisher?

好久不见. 提交于 2020-07-16 05:39:09

问题


I'm trying to write some unit tests for my API using URLSession.DataTaskPublisher. I've found an already existing question on Stackoverflow for the same but I'm struggling to implement a working class using the proposed solution.

Here's the existing question: How to mock URLSession.DataTaskPublisher

protocol APIDataTaskPublisher {
    func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher
}

class APISessionDataTaskPublisher: APIDataTaskPublisher {
    func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
        return session.dataTaskPublisher(for: request)
    }

    var session: URLSession

    init(session: URLSession = URLSession.shared) {
        self.session = session
    }
}

class URLSessionMock: APIDataTaskPublisher {
    func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
        // How can I return a mocked URLSession.DataTaskPublisher here?
    }
}

My API then uses the above like this:

class MyAPI {
    /// Shared URL session
    private let urlSession: APIDataTaskPublisher

    init(urlSession: APIDataTaskPublisher = APISessionDataTaskPublisher(session: URLSession.shared)) {
        self.urlSession = urlSession
    }
}

What I don't know is how to implement URLSessionMock.dataTaskPublisher().


回答1:


It would probably be simpler not to mock DataTaskPublisher. Do you really care if the publisher is a DataTaskPublisher? Probably not. What you probably care about is getting the same Output and Failure types as DataTaskPublisher. So change your API to only specify that:

protocol APIProvider {
    typealias APIResponse = URLSession.DataTaskPublisher.Output
    func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError>
}

Conform URLSession to it for production use:

extension URLSession: APIProvider {
    func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
        return dataTaskPublisher(for: request).eraseToAnyPublisher()
    }
}

And then your mock can create the publisher in any way that's convenient. For example:

struct MockAPIProvider: APIProvider {
    func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
        let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!
        let data = "Hello, world!".data(using: .utf8)!
        return Just((data: data, response: response))
            .setFailureType(to: URLError.self)
            .eraseToAnyPublisher()
    }
}



回答2:


If you store in UT bundle stub JSON (XML, or something) for every API call that you want to test then the simplest mocking code might look as following

class URLSessionMock: APIDataTaskPublisher {
    func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {

        // here might be created a map of API URLs to cached stub replies
        let stubReply = request.url?.lastPathComponent ?? "stub_error"
        return URLSession.shared.dataTaskPublisher(for: Bundle(for: type(of: self)).url(forResource: stubReply, withExtension: "json")!)
    }
}

so instead call to network server your publisher is created with URL of locally stored resource with known data, so you can verify all your workflow.




回答3:


Answered on original question, but will repost here:

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



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

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