2

I'm currently trying out a method of stubbing my network requests without needing to mock URLSession by using URLProtocol.

So far it works pretty well and functions by calling self.client?.urlProtocol(_ URLProtocol, didLoad: Data). However I'm unable to pass nil so I can test how my networking class handles being sent a successful response with no data (i.e. a HTTP 204 code). By default URLProtocol returns an empty Data object rather than nil so this does appear to be something I have to set manually.

Is there a way of returning a successful response with nil Data using a URLProtocol subclass? After searching the documentation I can't find anything that jumps out although it seems likely this is possible.

URLProtocolStub

// Custom URLProtocol to be used in place of Apple's HTTP protocols when
// testing network code
class URLProtocolStub: URLProtocol {
    static var testURLs = [URL?: Data]()

    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }

    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }

    override func startLoading() {
        if let url = request.url,
            let data = Self.testURLs[url] {
            self.client?.urlProtocol(self, didLoad: data)
        }
        client?.urlProtocolDidFinishLoading(self)
    }

    override func stopLoading() {}
}

Code to be tested

func requestCards(completionHandler:@escaping (Result<[Card],NetworkCallError>) -> Void ) {
    session.dataTask(with: URL(string: "https://deckofcardsapi.com/api/deck/new/draw/?count=52")!) { data, response, error in
        guard let data = data else {
            completionHandler(.failure(.noData))
            return
        }
Declan McKenna
  • 4,321
  • 6
  • 54
  • 72
  • 1
    Did you find a solution to mock that scenario? I'm facing the same issue – gmoraleda Aug 02 '21 at 08:19
  • @gmoraleda Unfortunately not. For this reason I don't use `URLProtocol` for stubbing my network requests and opt to create my own stub like the "complete stubbing" within this article. https://www.swiftbysundell.com/articles/mocking-in-swift/ – Declan McKenna Aug 02 '21 at 09:24

1 Answers1

1

Update the definition of testsURLs to allow for the response Data to be nil and call URLProtocolClient.urlProtocol(_:didFailWithError:) when it is.

Your startLoading() implementation would look like this:

static var testURLs = [URL?: Data?]()

override func startLoading() {
    if let url = request.url,
        let data = Self.testURLs[url] {
        self.client?.urlProtocol(self, didLoad: data)
    } else {
        let error = NSError(domain: "", code: 0, userInfo: nil)
        self.client?.urlProtocol(self, didFailWithError: error)
    }
    client?.urlProtocolDidFinishLoading(self)
}

Then add a test case:

myURLProtocolStub.testURLs["http://empty-data.com"] = nil
Paul F
  • 624
  • 6
  • 6
  • 1
    This is a good idea although I think once I use `didFailWithError` it won't return a successful response. I want to return `nil` data without an `error`. – Declan McKenna Jul 24 '20 at 14:36
  • 1
    This is so I can test handling 204 and sometimes 302 or 301 responses where I don't get an error or Data. I like the idea of not having to mock here but I can't find a way to configure this specific scenario. – Declan McKenna Jul 24 '20 at 15:15