Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock DataTaskPublisher?

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

like image 785
G. Marc Avatar asked Feb 06 '20 07:02

G. Marc


2 Answers

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()
    }
}
like image 193
rob mayoff Avatar answered Oct 19 '22 15:10

rob mayoff


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.

like image 2
Asperi Avatar answered Oct 19 '22 15:10

Asperi