Better Programming

Advice for programmers.

Follow publication

Writing Integration Tests for Your Network Layer Using URLProtocol

Harness the power of integration testing in your iOS apps

Karthik Shiva
Better Programming
Published in
5 min readMar 22, 2022

--

smartphone
Photo by Vojtech Bruzek on Unsplash

What Is Integration Testing?

This is where you run test cases when all your modules work together. In the real world, no module will ever function in isolation. There is always a flow of data between different modules. This is the reason bugs can occur even when all of your unit test cases pass. So, it is important to have integration tests in addition to unit tests.

Why Integration Testing?

Most of the apps out there communicate with some kind of backend. Particularly, the network layer is responsible for communicating with the backend and the data from this layer drives the application. Therefore, it is important that this layer functions properly, not only in isolation but also along with other modules.

What Is the Core Idea?

The core idea is to completely control the response (data), status code (HTTPURLResponse), and the error (error) that gets sent upwards from the network layer. By controlling these parameters, we can simulate different scenarios and check if our data parsing logic (decoding response), business layer logic (View model or Presenter) and all other modules behave as expected.

We can also examine the network requests made to our URLSession instance by our network layer and check if they are as expected.

Disclaimer: I have intentionally avoided best practices to keep the code simple for this article.

What Is URLProtocol?

Here we use a shared instance of the URLSession to make a network call. Usually, the system takes care of performing this network call and returns the result.

But the system also gives you a chance to perform the request yourself and return back the result. Using URLProtocol is the way to do it.

Let’s get started:

  1. Create a URLProtocol subclass and implement the above given mandatory methods
  2. Register your URLProtocol subclass with the system in your AppDelegate’s application (_:didFinishLaunchingWithOptions:) method.
  3. Assign the type of your URLProtocol subclass to the protocolClasses property of URLSession. Do not create an instance of your subclass since this is taken care of by the system automatically.

Now, whenever URLSession is requested to make a network request, it will first check with the objects in the protocolClasses array and ask if they want to handle the request by calling canInit(with: request) — > Bool. By returning true from our subclass, we will get a chance to handle the request.

Our Example App Structure

We have an app where a user can see a list of posts [String]. Let’s call it the user feed. We have three modules here:

1. Network layer

It uses the URLSession’s shared instance to make network requests and decodes the response using JSONDecoder.

2. View Model

It supports fetching and searching posts, using the network layer.

3. View Controller

It has an instance of the view model and conforms to the ViewModelDelegate protocol. It invokes the view model’s functions and receives a reply through delegate callbacks.

We can write unit test cases to verify every module’s behaviour in isolation, but what if they start behaving unexpectedly when they all start interacting with each other? This is why we will write a few integration tests to verify their behaviour.

Before we start writing our tests, let’s complete our URLSessionProxy, as shown below:

Don’t close the article 🤣. Let me break down what is going on:

startLoading()

This method is called by the system after we agree to handle an URL request by returning true inside func canInit(with request: URLRequest) -> Bool. We are supposed to start processing the request and inform the client when done.

RequestHandler

We will use this model object to set the response, data, or error for requests.

Static properties on URLSessionProxy

We have a few static properties that can be used to configure our class. They need to be static since the system takes care of creating the actual instance and we will not have access to this instance.

  1. contactedURLs: [URL]

This stores the list of URLs that are passed by the system. This array can be useful to examine the list of URLs that were accessed by our application.

2. shouldHandleRequest: ((URLRequest) -> Bool) = { _ in true }

You can use this closure to decide whether you want to handle a particular request. This is invoked inside func canInit(with request: URLRequest) -> Bool.

3. handleRequest: ((URLRequest) -> RequestHandler)?

This is invoked inside func startLoading(). You can use this closure to decide how you want to respond to an URLRequest by returning an instance of RequestHandler.

We're now all set! Let’s start writing the tests.

Test 1: User Feed API Call Is Fired on viewDidLoad()

Expectation: URLSessionProxy’s contactedURLs array contains the user feed API Url.

We configure our URLSessionProxy to return a default instance of RequestHandler instance since we are not focused on that part in this test. Then, we invoke viewDidLoad() on the view model to kick off the process.

We wait until our expectation has been fulfilled and then take advantage of our contactedURL’s property on URLSessionProxy to check if our app contacted the user feed API as expected.

Test 2: Users Can See Posts on Their Feed

Expectation: On invoking viewDidLoad(), view model contains mock posts after it calls reloadPosts() on its delegate.

We set up the following:

  1. Create a view model delegate and fulfill an expectation on the delegate method call, so that we know that its time to check the view model for the mock posts [String]
  2. Configure URLSessionProxy to return a mock response
  3. Call viewDidLoad() on the view model to start the process

When the expectation is fulfilled, we check if our view model contains our mock posts. This test will give us the confidence that both the network layer and view model are working together properly to display the posts to the user.

Test 3: Searching Posts Work Properly for Users

Expectation: Search API is called with the correct parameters

We configure our URLSessionProxy to capture the search URLRequest and save it to our local variable.

We then invoke the search method on our view model and then wait until our URLRequest is captured.

We then examine different parts of our URLRequest to check if all the parameters are as expected.

This is still scratching the surface with what you can do with URLSessionProxy. Hopefully, by now, you have formed an idea of how you can use this technique in your project.

The finished project is available here. Feel free to explore! There is also an open source library based on this technique.

--

--

Karthik Shiva
Karthik Shiva

Written by Karthik Shiva

iOS developer @ Gojek Super App — Let’s connect on LinkedIn: https://l.linklyhq.com/l/lWXS

No responses yet

Write a response