-
Notifications
You must be signed in to change notification settings - Fork 4
Mocking
You can mock endpoint responses for easy automated testing. There are high-level options.
- Expectation Setting – Set expectations around how endpoints should be called and define what will be returned from those endpoints.
- Custom Request Handling – Completely take over the request making process.
Expectation setting will be the most common mocking use-case. You start by indicating you would like to mock a particular instance of a web service and then set expectations about what requests should be made. Within those requests, you specify what the endpoint should return as long as the expectation is met.
To indicate you want to start mocking a service, call startMocking
.
let mock = MyService.shared.startMocking()
This does two important things:
- Stops requests from being made for real on this instance and instead requires expectations to be set.
- Returns a mock object to allow setting expectations.
Note: When a web service is being mocked, requests made when an expectation is not set, will result in an error. It is not possible for a real request to leak through.
When you are done mocking, you can return to allowing the service to make real requests.
MyService.shared.stopMocking()
All expectations are set on the mock object returned from startMocking
.
An empty endpoint can have an expectation that returns any successful or failure response.
// Return success
mock.expect(CheckStatus(), andReturn: .success)
// Return error
mock.expect(CheckStatus(), andReturn: .failure(DecreeError(.unauthorized)))
// Validate path
mock.expectEndpoint(
ofType: CheckStatus.self,
validatingPath: { path in
// Custom validation of the path
XCTAssertEqual(path, "check-status")
},
andReturn: .success
)
Note: If the endpoint has a customizable path
, this will be validated to match when the request comes in.
There are 3 types of expectations for In Endpoints.
// Validate the input exactly matches a fixed input
mock.expect(CreateObject(named: "my object"), receiving: "expected input")
// Throw an error
mock.expect(CreateObject(named: "my object"), throwingError: DecreeError(.unauthorized))
// Custom validation
mock.expect(CreateObject(named: "my object"), validatingInput: { input in
// Do any custom validation you want on input
// Return what the endpoint should return
return .failure(DecreeError(.unauthorized))
})
// Path validation
mock.expectEndpoint(
ofType: CreateObject.self,
validatingPath: { path in
// Custom validation of the path
XCTAssertEqual(path, "/object/new")
},
receiving: "expected input"
)
Note: If the endpoint has a customizable path
, this will be validated to match when the request comes in.
Out endpoints are very similar to Empty endpoints except in the success case, you need to return an Output instance.
/// Return a successful output
mock.expect(GetObject(objectId: 2), andReturn: .success(TestObject()))
// Return error
mock.expect(GetObject(objectId: 2), andReturn: .failure(DecreeError(.unauthorized)))
// Path validation
mock.expectEndpoint(
ofType: GetObject.self,
validatingPath: { path in
// Custom validation of the path
XCTAssertEqual(path, "object/2")
},
andReturn: .success(TestObject()))
)
// Expect download request
mock.expectDownload(GetObject(objectId: 2), andReturn: .success(TestObject()))
InOut endpoint expectations are similar to In endpoint expectations except in the success case, you need to return an Output instance.
// Validate the input exactly matches a fixed input and return success
mock.expect(Login(), receiving: .init(username: "user", password: "secret"), andReturn: .success(.init(token: "test-token")))
// Validate the input exactly matches a fixed input and return failure
mock.expect(Login(), receiving: .init(username: "user", password: "secret"), andReturn: .failure(DecreeError(.unauthorized)))
// Throw an error (without expecting any specific input)
mock.expect(Login(), throwingError: DecreeError(.unauthorized))
// Custom validation
mock.expect(Login(), validatingInput: { input in
// Do any custom validation you want on input
// Return what the endpoint should return
return .failure(DecreeError(.unauthorized))
})
// Path validation
mock.expectEndpoint(
ofType: Login.self,
validatingPath: { path in
// Custom validation of the path
XCTAssertEqual(path, "login")
},
receiving: .init(username: "user", password: "secret"),
andReturn: .success(.init(token: "test-token"))
)
// Download expectation
mock.expectDownload(Login(), andReturn: .success(.init(token: "test-token")))
Custom request handling is the more advanced form of mocking. It allows you to completely override all response from all requests made to all instances of a web service.
To start handling requests, you need an object that implements RequestHandler
.
class TestRequestHandler: RequestHandler {
func handle(dataRequest: URLRequest) -> (Data?, URLResponse?, Error?) {
return (nil, nil, nil)
}
func handle(downloadRequest: URLRequest) -> (URL?, URLResponse?, Error?) {
return (nil, nil, nil)
}
}
The first handler returns the following three things:
- An optional response body.
- An optional
URLResponse
(will usually be anHTTPURLResponse
) - An optional error
These are the 3 things that are usually returned from the underlying URLSession data task.
The second handler is very similar but it returns a URL to the download file instead of a response body.
With that setup, you can call the static startHandlingAllRequests(:)
method.
let handler = TestRequestHandler()
TestService.startHandlingAllRequests(handler: handler)
To stop handling requests, simply call stopHandlingAllRequests
.
MyService.stopHandlingAllRequests()