# HTTP Stubber - [HTTP Stubber](#http-stubber) - [Using HTTPStubber in your unit tests](#using-httpstubber-in-your-unit-tests) - [Stub a Request](#stub-a-request) - [Stub Matchers](#stub-matchers) - [Echo Matcher](#echo-matcher) - [Dynamic Matcher](#dynamic-matcher) - [URI Matcher](#uri-matcher) - [JSON Matcher](#json-matcher) - [Body Matcher](#body-matcher) - [URL Matcher](#url-matcher) - [Custom Matcher](#custom-matcher) - [Add Ignore Rule](#add-ignore-rule) - [Unhandled Rules](#unhandled-rules) - [Bad and down network](#bad-and-down-network) - [Simulate Network Conditions](#simulate-network-conditions) - [Simulate a down network](#simulate-a-down-network) 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: ```swift HTTPStubber.shared.enable() // enable the stubber ``` When finished, you can disable it: ```swift 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: ```swift 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). ```swift 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: ```swift 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: ```swift 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: ```swift 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](https://tools.ietf.org/html/rfc6570) - it uses the [URITemplate project by Kyle Fuller](https://github.com/kylef/URITemplate.swift). ```swift 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: - https://github.com/kylef/WebLinking.swift - https://github.com/kylef/{repository} - /kylef/{repository} - /kylef/URITemplate.swift ### JSON Matcher Say you're POSTing a JSON to your server, you could make your stub match a particular value like this: ```swift 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: ```swift 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: ```swift 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`: ```swift public protocol HTTPStubMatcherProtocol { func matches(request: URLRequest, for source: HTTPMatcherSource) -> Bool } ``` When you are ready, use `add()` to add the rule: ```swift 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`): ```swift 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. ```swift 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: ```swift 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: ```swift 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: ```swift let notConnectedError = NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue) let mock = try HTTPStubRequest().match(urlRegex: "(?s).*").stub(for: .get, error: notConnectedError) ... ```