Skip to content

Latest commit

 

History

History
318 lines (232 loc) · 13 KB

7.Stubber.md

File metadata and controls

318 lines (232 loc) · 13 KB

HTTP Stubber

RealHTTP offers elegant support for stubbing HTTP requests in Swift, allowing you to stub any HTTP/HTTPS requests made using URLSession. This means it works for requests made by libraries outside of RealHTTP as well, such as Alamofire.

To activate and configure the stubber you need to use the HTTPStubber.shared instance:

HTTPStubber.shared.enable() // enable the stubber

When finished, you can disable it:

HTTPStubber.shared.disable()

Using HTTPStubber in your unit tests

HTTPStubber is ideal to write unit tests that normally would perform network requests.
But if you do use it in your unit tests, don't forget to:

  • remove any stubs you installed after each test by calling HTTPStubber.removeAllStubs() in your tearDown method, in order to avoid having those stubs remain active during execution of the next Test Case.
  • be sure to wait until the request has received its response before doing your assertions and letting the test case finish (like for any asynchronous test).

When you use HTTPStubber to stub network requests for your Unit Tests, keep in mind that each stub that you have added in a test case is still in place when the next test executes.

We strongly suggest you remove all stubs after each of your test cases to be sure they don't interfere with other test cases: therefore, tearDown (executed after each test case) is the ideal place to reset your stubber:

func tearDown() {
  super.tearDown()
  HTTPStubber.removeAllStubs()
}

As HTTPStubber is a library generally used in Unit Tests invoking network requests, those tests are generally asynchronous.
This means that the network requests are generally sent asynchronously, in a separate thread or queue from the one used to execute the test case.

As a consequence, you will have to ensure that your Unit Tests wait for the requests to finish (and their responses have arrived) before performing the assertions in your Test Cases, otherwise your assertions will execute before the request had time to get its response.

The best solution is to use XCTestExpectation or to use the await of fetch() if you are using a RealHTTP client.

Stub a Request

To stub a request, first you need to create an HTTPStubRequest and an HTTPStubResponse.
You then register this stub with RealHTTP and tell it to intercept network requests by calling the enable() method.

An HTTPStubRequest describes what kind of triggers should be matched in order to activate the stub request; a single trigger is called a matcher which is an entity that conforms to the HTTPStubMatcherProtocol.
Only when all matchers of a stub request are validated will the stub be used to mock the request; in which case, the associated response is used.
You can specify a different HTTPStubResponse for each HTTP Method as well (ie. a different response for GET and POST using the same trigger).

var stub = try HTTPStubRequest()
           .match(urlRegex: "http://www.google.com/+") // stub any url for google domain
           .stub(for: .post, delay: 5, json: jsonData) // stub the post with json data and delay
           .stub(for: .put, error: internalError) // stub error for put

// Add to the stubber
HTTPStubber.shared.add(stub: stub)

The preceding example uses the URL Regex Matcher to match any url of the google domain.
The stub returns

  • a jsonData for POST requests with a delay of the response of 5 seconds
  • an internalError for any PUT request

an HTTPStubResponse is an object associated with an HTTPStubRequest for a particular HTTP Method. Each object can be configured to return custom data such as:

  • statusCode: the HTTP Status Code
  • contentType: the content-type header of the response
  • failError: when passed a non nil error, the Error is stubbed as the response
  • body: the body of the response
  • headers: headers to return with the response
  • cachePolicy: cache policy to trigger for resposne
  • responseDelay: delay interval in seconds before responding to a triggered request. Very useful for testing.

While you can use any of the shortcuts in HTTPStubRequest's stub(...) method to set a concise HTTPStubResponse you can also provide a complete object to specify the properties above:

var stub = HTTPStubRequest()
           .stub(for: .post, { response in
                // Builder function allows you to specifiy any property of the
                // HTTPStubResponse object for this http method.
                response.responseDelay = 5
                response.headers = HTTPHeaders([
                    .contentType: HTTPContentType.bmp.rawValue,
                    .contentLength: String(fileSize),
                ])
                response.body = fileContent
            })

Stub Matchers

There are different stub matchers you can chain for a single stub request.

NOTE: matchers are evaluated with an AND operator, not an OR. So all matchers must be verified in order to trigger the associated request.

Echo Matcher

A special stubber is HTTPEchoResponse which basically responds with the same request received.
This is particularly useful when you need to check and validate your requests.

To enable it:

let echoStub = HTTPStubRequest().match(urlRegex: "*").stubEcho()
HTTPStubber.shared.add(stub: echoStub)
HTTPStubber.shared.enable()

Dynamic Matcher

You may want to customize the HTTPStubResponse based upon certain parameters of the received URLRequest/HTTPStubRequest.
RealHTTP offers a way to return the appropriate stub response based upon the received request by using the stub(method:responseProvider:) method which creates an HTTPDynamicStubResponse instance.

The following example captures the login call and then personalizes the HTTPStubResponse to return:

let stubLogin = HTTPStubRequest().match(urlRegex: "/login").stub(for: .post, responseProvider: { urlRequest, matchedStubRequest in
    let response = HTTPStubResponse()
            
    guard let dict = try! JSONSerialization.jsonObject(with: urlRequest.body!) as? NSDictionary,
          let username = dict["user"] as? String else {
      response.statusCode = .unauthorized
      return response
    }
            
    if username == "mark" {
       // configure the response for mark, our special admin!
    } else {
      // ... return another response for anyone else!
    }
            
    return response
})

URI Matcher

RealHTTP allows you to define a URI matcher (HTTPStubURITemplateMatcher) which is based upon the URI Template RFC - it uses the URITemplate project by Kyle Fuller.

var stub = HTTPStubRequest()
           .match(URI: "https://github.com/kylef/{repository}")
           .stub(...)

The uri function takes a URL or path which can have a URI Template. Such as the following:

JSON Matcher

Say you're POSTing a JSON to your server, you could make your stub match a particular value like this:

var stub = HTTPStubRequest()
           .match(object: User(userID: 34, fullName: "Mark"))

It will match the JSON representation of an User struct with userID=34 and fullName=Mark without representing the raw JSON but instead just passing the Codable struct!
It uses the HTTPStubJSONMatcher matcher.

Body Matcher

Using the HTTPStubBodyMatcher you can match the body of a request which should be equal to a particular value in order to trigger the relative stub request:

let bodyToCheck: Data = ...
var stub = HTTPStubRequest()
           .match(body: bodyToCheck)
           .stub(...)

URL Matcher

The URL matcher HTTPStubURLMatcher is a simple version of the regular expression matcher which check the equality of an URL by including or ignoring the query parameters:

var stub = HTTPStubRequest()
           .match(URL: "http://myws.com/login", options: .ignoreQueryParameters)

It will match URLs by ignoring any query parameter like http://myws.com/login?username=xxxx or http://myws.com/login?username=yyyy&lock=1.

Custom Matcher

You can create your custom matcher and add it to the matchers of a stub request. It must conform to the HTTPStubMatcherProtocol:

public protocol HTTPStubMatcherProtocol {
    func matches(request: URLRequest, for source: HTTPMatcherSource) -> Bool
}

When you are ready, use add() to add the rule:

struct MyCustomMatcher: HTTPStubMatcherProtocol { ... } // implement your logic

stub.match(MyCustomMatcher()) // add to the matcher of a request

Sometimes you may not want to create a custom object to validate your matcher (HTTPStubCustomMatcher):

var stub = HTTPStubRequest()
           .match({ req, source in
              req.headers[.userAgent] == "customAgent"
           })
           .stub(...)

The following stub is triggered when headers of the request is customAgent.

Add Ignore Rule

You can add an ignoring rule to the HTTPStubber.shared instance in order to ignore and pass through some URLs.
An ignore rule is an object of type HTTPStubIgnoreRule which contains a list of matchers: when all matchers are verified the stub is valid and the request is ignored. It works like stub request for response but for ignores.

HTTPStubber.shared.add(ignoreURL: "http://www.apple.com", options: [.ignorePath, .ignoreQueryParameters])

You can use all the matchers described above even for rules.

Unhandled Rules

You can choose how RealHTTP must deal with stub not found. You have two options:

  • optout: only registered URLs which match the matchers are ignored for mocking. - Registered mocked URL: mocked. - Registered ignored URL: ignored by the stubber, default process is applied as if the stubber is disabled. - Any other URL: Raises an error.
  • optin: Only registered mocked URLs are mocked, all others pass through. - Registered mocked URL: mocked. - Any other URL: ignored by the stubber, default process is applied as if the stubber is disabled.

To set the behaviour:

HTTPStubber.shared.unhandledMode = .optin // enable optin mode

Bad or down network

Simulate Network Conditions

Simulating slow or bad connections is very important when you develop a mobile application.
For example you can check that your user interface does not freeze when you have bad network conditions, and that you have all your activity indicators working while waiting for responses.

HTTPStubber allows you to simulate different network conditions using the responseTime property of each HTTPStubResponse instance.
responseTime is of type HTTPStubResponseInterval which is an enum with the following options:

  • immediate: data is sent back to the client immediately with no delay (this is the default behavior).
  • delayedBy(TimeInterval): data is sent back to the client after a specified number of seconds.
  • withSpeed(HTTPConnectionSpeed): data is sent back to the client with the specified connection speed (the sending of the fake response will be spread over time. This allows you to simulate a slow network, for example)

HTTPConnectionSpeed can be:

  • speed1kbps: 1 kpbs
  • speedSlow: 12 kpbs (1.5 KB/s)
  • speedGPRS: 56 kbps (7 KB/s)
  • speedEdge: 128 kbps (16 KB/s)
  • speed3G: 3200 kbps (400 KB/s)
  • speed3GPlus: 200 kbps (900 KB/s)
  • speedWiFi: 12000 kbps (1500 KB/s)

Example:

let mock = try HTTPStubRequest()
            .match(urlRegex: "(?s).*")
            .stub(for: .get, {
                $0.statusCode = .ok
                $0.responseTime = .withSpeed(.speed1kbps) <--- VERY SLOW CONNECTION
                $0.contentType = .text
                $0.body = randomData
                $0.headers = [
                    "Content-Length": String(randomData.count)
                ]
            })
HTTPStubber.shared.add(stub: mock)

Simulate a down network

You may also return a network error for your stub.
For example, you can easily simulate an absence of network connection like this:

let notConnectedError = NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue)
let mock = try HTTPStubRequest().match(urlRegex: "(?s).*").stub(for: .get, error: notConnectedError)
...