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()
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 yourtearDown
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.
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
forPOST
requests with a delay of the response of 5 seconds - an
internalError
for anyPUT
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 CodecontentType
: the content-type header of the responsefailError
: when passed a non nil error, theError
is stubbed as the responsebody
: the body of the responseheaders
: headers to return with the responsecachePolicy
: cache policy to trigger for resposneresponseDelay
: 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
})
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.
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()
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
})
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:
- https://github.com/kylef/WebLinking.swift
- https://github.com/kylef/{repository}
- /kylef/{repository}
- /kylef/URITemplate.swift
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.
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(...)
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
.
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
.
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.
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
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 kpbsspeedSlow
: 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)
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)
...