Often times when trying to stub network requests, there are relatively complex requirements. For example, if your app uses an access and refresh token and you have 3 responses, you'd probably like to stub first an unauthorized response, then a successful refresh, then a successful response. While there are a couple of stubbing libraries out there, their solutions tend to be frustratingly limited. 

Stubbing solution

I'll pick on OHHTTPStubs as a library because it's one of the more popular ones and seems to stay maintained. Their docs suggest this solution:

// Swift
var callCounter = 0
stub(…) { request in
  callCounter += 1
  if callCounter <= 2 {
    let notConnectedError = NSError(domain:NSURLErrorDomain, code:Int(CFNetworkErrors.CFURLErrorNotConnectedToInternet.rawValue), userInfo:nil)
    return OHHTTPStubsResponse(error:notConnectedError)
  } else {
    let stubData = "Hello World!".dataUsingEncoding(NSUTF8StringEncoding)
    return OHHTTPStubsResponse(data: stubData!, statusCode:200, headers:nil)
  }
}

Ahhh! Look at all that state you have to maintain. That's also not taking into account assertions you want to make on the request used. In my previous scenario, this stub could get really out of control in the test setup. 

What's more, if we ever want to change the order of stubs, we have our work cut out for us. I think we can do better.

A fluent API lends itself quite well to solving these problems, and I think an interface like this would be really nice:

StubAPIResponse(request: .init(.get, urlString: "\(baseURL)/me"),
                        statusCode: 401)
            .thenRespondWith(request: .init(.post,
                                            urlString: "\(baseURL)/auth/refresh"),
                             statusCode: 200,
                             result: .success(validRefreshResponse))
            .thenVerifyRequest { request in
                //assertions on request attributes like method, headers, and body
            }
            .thenRespondWith(request: .init(.get, urlString: "\(baseURL)/me"),
                             statusCode: 200,
                             result: .success(validProfileJSON.data(using: .utf8)!))
            .thenVerifyRequest { request in
                //assertions on request attributes like method, headers, and body
            }

If we ever need to move around the order of responses, it's as simple as moving the lines of code. If we want to assert information about the actual request used we can do that, and since this is all done with closures, if this starts to take up too much vertical space it'd be pretty trivial to extract to some private functions.

So how do we pull something like this off? Let's start simple. Let's make creating URLRequests easy to do in one line.

extension URLRequest {
    enum HTTPMethod {
        case get
        case put
        case post
        case patch
        case delete
    }
    
    init(_ method:HTTPMethod, urlString: String) {
        let url = URL(string: urlString)
        self.init(url: url!)
        httpMethod = {
            switch method {
            case .get: return "GET"
            case .put: return "PUT"
            case .post: return "POST"
            case .patch: return "PATCH"
            case .delete: return "DELETE"
            }
        }()
    }

This makes using Http verbs much simpler, which makes our stubbing API friendlier. However, as long as we're extending URLRequest there's another convenience we can give ourselves. If we try to make assertions using the httpBody property on URLRequest we'll find it is always nil. This has everything to do with the lifecycle of when that gets set but makes testing annoying. Let's create a function that gives us the body of a request, so we can make assertions on it.

extension URLRequest {
    //... convenience init from earlier

    func bodySteamAsData() -> Data? {
        guard let bodyStream = self.httpBodyStream else { return nil }
        
        bodyStream.open()
        
        // Will read 16 chars per iteration. Can use bigger buffer if needed
        let bufferSize: Int = 16
        let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
        var data = Data()
        
        while bodyStream.hasBytesAvailable {            
            let readData = bodyStream.read(buffer, maxLength: bufferSize)
            data.append(buffer, count: readData)
        }

        buffer.deallocate()
        bodyStream.close()
        return data
    }
}

Great! Now let's move on to the actual fluent wrapper. 

We need a way of holding onto the responses we want to send, the data we want to send and the verifier blocks that make assertions about the actual request used. There are many ways of doing this, but let's start by coming up with a way to uniquely identify requests.

extension URLRequest: Identifiable {
    public var id:String {
        [httpMethod, url?.absoluteString].compactMap { $0 }.joined(separator: "_")
    }
}

Great! Now we can store all the data we need in a dictionary, with the ID of the request as the key.

class StubAPIResponse {
    var results = [String: [Result<Data, Error>]]()
    var responses = [String: [HTTPURLResponse]]()
    var requests = [URLRequest]()
    var verifiers = [String: ((URLRequest) -> Void)]()
    
    // ...
}

Then there's a desire that once stubbed, an endpoint should stay stubbed. In other words, if I have a chain like this:

StubAPIResponse(request: .init(.get, urlString: "\(baseURL)/me"),
                        statusCode: 401)
            .thenRespondWith(request: .init(.get, urlString: "\(baseURL)/me"),
                             statusCode: 200,
                             result: .success(validProfileJSON.data(using: .utf8)!))

The first time the endpoint is called it should return a 401, and no matter how many times it's called after that it should always return a 200. Since we're popping values out of an array, let's quickly make an extension that only pops if the array has more than one item in it:

fileprivate extension Array {
    mutating func popLastUnlessEmpty() -> Element? {
        if (count > 1) {
            return popLast()
        } else {
            return last
        }
    }
}

Next, let's create our initializer and our thenRespondWith fluent method, and our thenVerify fluent method.

class StubAPIResponse {
    @discardableResult init(request:URLRequest, statusCode:Int, result:Result<Data, Error>? = nil, headers:[String : String]? = nil) {
        thenRespondWith(request: request,
                        statusCode: statusCode, result: result,
                        headers: headers)
    }
    
    @discardableResult func thenRespondWith(request:URLRequest, statusCode:Int, result:Result<Data, Error>? = nil, headers:[String : String]? = nil) -> Self {
        guard let url = request.url else { return self }
        if let res = result {
            results[request.id, default: []].insert(res, at: 0)
        }
        responses[request.id, default: []].insert(HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: "2.0", headerFields: headers)!, at: 0)
        requests.insert(request, at: 0)
        
        //this uses the OHHTTPStubs library, but almost any stubbing library can be used.
        stub(condition: isAbsoluteURLString(url.absoluteString)) { [self] in
            if let verifier = verifiers[$0.id] {
                verifier($0)
            }
            let response = responses[$0.id]!.popLastUnlessEmpty()!
            let result = results[$0.id]!.popLastUnlessEmpty()!
            switch result {
            case .failure(let err): return HTTPStubsResponse(error: err)
            case .success(let data): return HTTPStubsResponse(data: data, statusCode: Int32(response.statusCode), headers: response.allHeaderFields)
            }
        }

        return self
    }
    
    @discardableResult func thenVerifyRequest(_ requestVerifier:@escaping ((URLRequest) -> Void)) -> Self {
        guard let req = requests.first else { return self }
        verifiers[req.id] = requestVerifier
        return self
    }
}

There we go! 

Now we've got a handy fluent wrapper for our stubbing library. That'll make it a lot easier to build up stubs.

Learn more about our software expertise.
Explore

Technologies