Testable Network layer in Swift — Part 1

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 can not be tested is flawed.”

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.

A network layer consists of:

  • Prepare Request (url, method type, headers, parameters)
  • API call (NSURLSession)
  • 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.

LoginAPI example

Path() — 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.

setAs the above sample API call is a post method, we need to prepare httpBody, which is done through the RequestHandler protocol extension.

set httpBody

BaseRequest — 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.

Generic parser

The above code handles api response with success, known-error and unknown-error from the server

API Call

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.

Generic APILoader

We can pass the LoginAPI object to the generic APILoader as shown below.

LoginService function

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.

Generic Error

Depending on the requirement, we can handle specific error types as shown below:

Handle 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.

You can find the source code below:

This is based on WWDC 2018: “Testing Tips & Tricks” — Thanks to Brian Croom from Apple.

Once your code had decoupled the network layer as said above, your code is now ready to be tested. You can know how to write tests on network layer in Part — 2.

--

--