Swift - Protocol can only be used as a generic constraint because it has Self or associated type requirements
You have to turn these requirements around;
Instead of injecting a MicroServiceProvider into each request, you should write a generic MicroService 'Connector' Protocol that should define what it expects from each request, and what each request expects it to return.
You can then write a TestConnector which conforms to this protocol, so that you have complete control over how your requests are handled. The best part is, your requests won't even need to be modified.
Consider the following example:
protocol Request { // What type data you expect to decode and return associatedtype Response // Turn all the data defined by your concrete type // into a URLRequest that we can natively send out. func makeURLRequest() -> URLRequest // Once the URLRequest returns, decode its content // if it succeeds, you have your actual response object func decode(incomingData: Data?) -> Response?}protocol Connector { // Take in any type conforming to Request, // do whatever is needed to get back some potential data, // and eventually call the handler with the expected response func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void)}
These are essentially the bare minimum requirements to setup such a framework. In real life, you'll want more requirements from your Request protocol (such as ways to define the URL, request headers, request body, etc).
The best part is, you can write default implementations for your protocols. That removes a lot of boilerplate code! So for an actual Connector, you could do this:
extension Connector { func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void) { // Use a native URLSession let session = URLSession() // Get our URLRequest let urlRequest = request.makeURLRequest() // define how our URLRequest is handled let task = session.dataTask(with: urlRequest) { data, response, error in // Try to decode our expected response object from the request's data let responseObject = request.decode(incomingData: data) // send back our potential object to the caller's completion block handler(responseObject) } task.resume() }}
Now, with that, all you need to do is implement your ProfilePictureRequest like this (with extra example class variables):
struct ProfilePictureRequest: Request { private let userID: String private let useAuthentication: Bool /// MARK: Conform to Request typealias Response = UIImage func makeURLRequest() -> URLRequest { // get the url from somewhere let url = YourEndpointProvider.profilePictureURL(byUserID: userID) // use that URL to instantiate a native URLRequest var urlRequest = URLRequest(url: url) // example use: Set the http method urlRequest.httpMethod = "GET" // example use: Modify headers if useAuthentication { urlRequest.setValue(someAuthenticationToken.rawValue, forHTTPHeaderField: "Authorization") } // Once the configuration is done, return the urlRequest return urlRequest } func decode(incomingData: Data?) -> Response? { // make sure we actually have some data guard let data = incomingData else { return nil } // use UIImage's native data initializer. return UIImage(data: data) }}
If you then want to send a profile picture request out, all you then need to do is (you'll need a concrete type that conforms to Connector, but since the Connector protocol has default implementations, that concrete type is mostly empty in this example: struct GenericConnector: Connector {}
):
// Create an instance of your request with the arguments you desirelet request = ProfilePictureRequest(userID: "JohnDoe", useAuthentication: false)// perform your request with the desired ConnectorGenericConnector().perform(request) { image in guard let image = image else { return } // You have your image, you can now use that instance whichever way you'd like ProfilePictureViewController.current.update(with: image)}
And finally, to set up your TestConnector, all you need to do is:
struct TestConnector: Connector { // define a convenience action for your tests enum Behavior { // The network call always fails case alwaysFail // The network call always succeeds with the given response case alwaysSucceed(Any) } // configure this before each request you want to test static var behavior: Behavior func perform<T: Request>(request: T, handler: @escaping (T.Response?) -> Void) { // since this is a test, you don't need to actually perform any network calls. // just check what should be done switch Self.behavior { case alwaysFail: handler(nil) case alwaysSucceed(let response): handler(response as! T) } }}
With this, you can easily define Requests, how they should configure their URL actions and how they decode their own Response type, and you can easily write mocks for you connectors.
Of course, keep in mind that the examples given in this answer are quite limited in how they can be used. I would highly suggest you to take a look at this library I wrote. It extends this example in a much more structured way.