Search code examples
iosswiftunit-testingswift4urlsession

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

Solution

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