Как издеваться над DataTaskPublisher? - PullRequest
1 голос
/ 06 февраля 2020

Я пытаюсь написать несколько модульных тестов для моего API, используя URLSession.DataTaskPublisher. Я нашел уже существующий вопрос по Stackoverflow для того же, но я изо всех сил пытаюсь реализовать рабочий класс, используя предложенное решение.

Вот существующий вопрос: Как смоделировать 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?
    }
}

Затем мой API использует вышеуказанное:

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

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

Чего я не знаю, так это как реализовать URLSessionMock.dataTaskPublisher ().

Ответы [ 3 ]

0 голосов
/ 05 мая 2020

Ответили на оригинальный вопрос, но перепишут здесь:

Поскольку 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 (этот утверждает, что URLError s сопоставлены с пользовательской ошибкой, но вы также можете утверждать некоторые другие условия, данные / ответ):

  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, поэтому вы должны жить с предупреждением в своем тесте. Если кто-то может найти способ обойти это, я был бы очень признателен.

(Примечание: я использую Пересмешник для насмешек).

0 голосов
/ 06 мая 2020

Вероятно, было бы проще не издеваться DataTaskPublisher. Вас действительно волнует, является ли издатель DataTaskPublisher? Возможно нет. Что вас, вероятно, волнует, так это получение тех же типов Output и Failure, что и DataTaskPublisher. Поэтому измените свой API, указав только:

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

Соответствие URLSession ему для производственного использования:

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

И тогда ваш макет может создать издателя любым удобным для вас способом. , Например:

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()
    }
}
0 голосов
/ 08 февраля 2020

Если вы сохраняете в заглушке комплекта UT JSON (XML или что-то) для каждого вызова API, который вы хотите протестировать, тогда самый простой код насмешки может выглядеть следующим образом

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

, поэтому вместо этого Для вызова сетевого сервера ваш издатель создан с URL-адресом локально сохраненного ресурса с известными данными, поэтому вы можете проверить весь свой рабочий процесс.

...