Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to stub URLSession in Swift?

I have been following this tutorial to stub out URLSession. The example was done by creating a protocol and extending the existing URLSession.

protocol URLSessionProtocol {
    typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void
    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol
}

extension URLSession: URLSessionProtocol {
    func dataTask(with request: NSURLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {
        return dataTask(with: request, completionHandler: completionHandler) as URLSessionDataTaskProtocol
    }
}

The unit tests work as expected. But when I try to run the real thing, the URLSession -> datatask() gets into an infinite loop and it crashes. It seems to be that datatask() is calling itself.

What am I overlooking, please?

UPDATE:

protocol URLSessionDataTaskProtocol {
    var originalRequest: URLRequest? { get }
    func resume()
}

extension URLSessionDataTask: URLSessionDataTaskProtocol {}
like image 848
Houman Avatar asked Mar 27 '18 08:03

Houman


2 Answers

I have finally found the solution. It’s fascinating as we missed the wood for the trees. There are two issues:

1) It seems that Swift 4 has changed the signature for dataTask(with: NSURLRequest) to dataTask(with: URLRequest)

Therefore the line in my opening question would only match to the Protocol's func signature, and it would never hit the dataTask inside URLSession, hence the infinite loop. To solve this issue I had to change NSURLRequest to URLRequest and refactor the code accordingly.

2) The signature remains vague, hence it is better to store the result as dataTask first with a cast to URLSessionDataTask and then return the variable.

New refactored Code for Swift 4:

typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void

protocol URLSessionProtocol {
    func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol
}

extension URLSession: URLSessionProtocol {
    func dataTask(with request: URLRequest, completionHandler: @escaping DataTaskResult) -> URLSessionDataTaskProtocol {
        let task:URLSessionDataTask = dataTask(with: request, completionHandler: {
            (data:Data?, response:URLResponse?, error:Error?) in completionHandler(data,response,error) }) as URLSessionDataTask
        return task
    }
}

I also found I had to inject URLSession.shared as a singleton and not as URLSession(), otherwise it could crash.

like image 179
Houman Avatar answered Oct 15 '22 03:10

Houman


Came here to understand how to mock URLSession tasks such as URLSessionDataTask?

Circa Swift 5, it is much easier to mock the URLProtocol that URLSession is using to send the request.

See Unit Testing URLSession using URLProtocol.

like image 2
Donal Lafferty Avatar answered Oct 15 '22 04:10

Donal Lafferty