Using Swift Generics & Protocol Extensions to Decouple Network Layer Code
When you write a program, the code should be readable, maintainable and testable, as per coding best practices.
“A code that cannot be tested is flawed.” – Anonymous
As iOS mobile app developers, we often write a lot of code that deals with UI, network, persistence and other business logic. In this article, we will share our implementation of Network Layer, which deals with API /web-service interactions, to help to write tests on network layer logic.
Before writing tests, we need to become familiar with how to decouple Network Layer code from UI related code and other business logic. Without this decoupling, it would be impossible to test the network layer in isolation.
Parse Response (convert data into model objects (or) return error message)
Source: Apple WWDC 2018: Testing Tips & Tricks
Network layer tests allow us to ensure that the API request has been formed correctly and that API response parsing has been done as expected, mocking a web server.
Preparing a Request
Based on our test scenarios, we will need to further decouple the network layer. To do that, we’ll create an APIHandler, which is used to make a request and parse the response.
Request and Response Protocols
See below for a sample request/response handler for a LoginAPI by conforming to the APIHandler.
Don’t worry about the Path().login. Path() is just a method that returns the specific endpoint as per the DEV / TEST / RELEASE environments. More details can be found here.
All API requests would contain url, httpMethod, parameters and headers.
As the above sample API call is a post method, we need to prepare httpBody, which is done through the RequestHandler protocol extension.
For all the common request configurations, like headers, timeoutInterval, etc., we can create a class BaseRequest that conforms to the Request Protocol,as shown below.
Common Request Configurations
Once we have set the common configurations, each API may have custom parameters to be sent in the API request. For any API that requires an authentication token, we can use AuthRequest object instead of BaseRequest object so that the API request has the auth-token.
Authenticated Request Configurations
Now we have our URLRequest prepared as required.
Parsing a Response
Once the API request is prepared, we can call the API (which we will walk through in a minute). Once the API call is placed and the server responds, we will receive a raw response that needs to be parsed as per our requirement. Generally, we parse the raw response into model objects. To do so, we can use generics to handle the response in ResponseHandler.
The above code handles the API response with success, known-error and unknown-error from the server.
To call an API, we have written a generic class APILoader that handles network errors, URLSession and Internet connection errors using the reachability library, as shown below.
We can pass the LoginAPI object to the generic APILoader as shown below.
We can also group user related API calls like login, userDetails and deleteUser methods in a separate swift file, like UserServices.swift, for better readability.
Interacting with NetworkLayer
When a user interacts with your app and your UI requires some data from the backend, we have to interact with NetworkLayer. Most often the interaction source would be from the Presentation/Business/Repository layer. This layer should interact with UserServices.swift file methods to get data from the server through an API call, as seen below.
Depending on the requirement, we can handle specific error types as shown below:
Handling Specific Errors
Regarding makeRequest(params…) in the LoginAPI struct above: remember that if we try to make this function more generic by passing url, http method, etc., along with required input parameters, we would be violating the Single Responsibility Principle, which states that only the service layer should be responsible for creating the request, as it is not the business/repo layers’ responsibility to handle the services. This way, the service layer is completely isolated from calling components and is testable, too.
Once your code has decoupled the network layer as described in this article, it is ready to be tested. Part 2 of this series (coming soon) will show you how to write tests on NetworkLayer.