Better network layer on iOS with synchronous calls
In most iOS apps, as app developers, we pay a very special attention to the user interface. For example, we try to perfectly implement the graphical design as proposed by the designer. But on the other hand, we pay very little attention to the other parts of the app such as the the data management or the network layer.
In this article, I will explain why, during my last project, as we were trying to improve the unit tests of our network calls, we decided to switch our networking calls to synchronous ones and how this produced some desirable side effects.
1. Current networking situation
For this article, I will take as an example a simple app displaying a list of posts. The user cam mark some of them as favorites. This app uses an MVP architecture:
- The Model is composed of two services: PostsService which gets the posts and UserService which can add a post to user favorites and get the user’s favorite posts ids. These two services use a networking library such as Alamofire for the network calls.
- The View is a passive view displaying the posts in a table view.
- A Presenter PostsListPresenter which contains the logic behind the view.
Usually, we have two strategies to write asynchronous services calling web endpoints. Either a simple completion callback containing the entity and the error, or two separate callbacks for success and failure:
These two strategies work in the real world but have some flaws. For example for the first method it is unclear what should happen when both posts and error are nil. And for the second, we often are too lazy to implement both callbacks so we end up making the failure optional and we stop handling the errors properly.
These flaws are not as severe as the real implementation which is often tightly coupled to a network library making them hard to unit test.
1.2. Unit tests
In order to have control over the server response we have two options. Either implement a custom NSURLProtocol, register it to intercept the calls and override the responses or use an external library such as OHHTTPStubs. After that, we have to setup an XCTestExpectation to wait for the asynchronous call to respond. And finally we check that the received objects are equal to the expected ones.
This gives us the following unit test:
This test is not easy to read and not easy to understand for somebody joining the project. In addition if we ask new members to unit test their service call, they are likely not to be rejoiced and may even take it as some kind of hazing.
To overcome this complexity, we started thinking how an easy to read unit test should look like.
1.3. Ideal target
The complexity of the unit test is caused by the tight coupling to the networking library and the asynchronicity of the service. Thus we had to get rid of these two issues. The simplest unit test does some setup, executes a function and checks the output. So we thought that it would be nice if our unit test looked like this:
As you may have noticed, to be able to write the test this way, we will have to make the service return the posts in a synchronous way.
2. Journey to better network calls
2.1. Regaining control
To do this, we use the Dependency Inversion from the SOLID principles and introduce a new API protocol that we will be using in the services. The implementation of this protocol can use NSURLSession directly or any other available networking library.
The APIRequest object contains all the necessary information to execute the request such as the path and the HTTP method for example. The APIResponse is composed of the received data or the error.
2.2. Implementing the API protocol
The principal difficulty here is that all the tools we have at our disposal are asynchronous. So we had to find a way to make asynchronous functions synchronous. And we were lucky to find out that there was a simple way to do this with Apple Dispatch framework using the DispatchGroup class.
Here is an example of the API implementation using Alamofire:
2.3. Unit Tests
As we use the TDD (Test Driven Development) methodology, we start by writing the tests and then we write as little code as possible to make the tests pass. In our case we have two types of tests: the first ones will verify that we call the API with the correct APIRequest and the second type will verify the value returned by the service depending on the stubbed server response.
To handle the two types of tests we start by writing a MockAPI implementing the API protocol. This mock will retain the request parameter so we could verify it and it returns a response by reading a local file:
At this point we also have to think about how will we handle the errors. We chose to use a generic enum Result (similar to the Optional enum) having two cases: a generic success and a failure.
With all these elements, we get the following test verifying that the service correctly parses the server response:
This is much nicer to read and easier to understand.
2.4. Using the protocol in the services
We start by injecting the protocol implementation in the service in the init method. Then in the getPostsList function, we build the request, use it in the API to get the response and finally parse it:
2.5. Using the services in the presenter
This step is pretty straightforward but we have to make sure not to block the main thread. So we start by switching to a background thread before calling the different services and we go back to the main thread before updating the view:
After this last step, everything should work as before. And in addition to that, our network layer is fully tested and regression-proof.
Moving the asynchronicity management to the presenter will eventually complexify its unit tests. This is due to the architecture we choose as an example for this article. If the presenter becomes more complex and difficult to maintain, we could imagine migrating our project to a more decoupled architecture such as VIPER. This way, all the logic is contained in a synchronous interactor which is easy to unit test. And the presenter will only be responsible of calling the interactor and the asynchronicity management.
3. (Desirable) Side effects
Developing all our network calls this way has produced some desirable side effects which are listed below.
3.1. Mock environment
If the feature in the iOS app is ready before it is available in the backend, we can still show it to our product manager or client for validation purposes. Moreover the files that we used to stub the server’s responses can be used as a reference for the backend development. And we can update them after the backend is ready to check that everything is working as expected.
3.2. Better interface tests and up to date screenshots
Having this stubbed API enables us to have better UI tests by making us able to test all the edge cases if we want to. It also makes it easier to have up to date app screenshots if we choose to automate them using fastlane/snaphot.
3.3. Integration tests
The synchronous services make it easier to have integration tests that will call the app implementation of the API protocol. This will help us check that we are calling the backend and that it is responding as expected.
If it is not possible to test all the endpoints, we can imagine having a critical scenario running regularly on an integration server and checking that there is no major issue. For example:
4. One more thing…
Chris Lattner recently published a manifesto on github. His goal was to start a conversation about what features should the Swift language offer to handle concurrency. He proposed an approach consisting of introducing the async and await keywords. An asynchronous function will have the async keyword in its declaration and when we call it we add the await keyword to wait for the response.
The publication of this manifesto reassured us about the direction we took when we were trying to improve our network calls and made us look forward to a better concurrency handling in the Swift language.