The guide to unit testing in Swift with Apollo and GraphQL Part One

Alexander Murphy
4 min readDec 16, 2018

--

As iOS developers, we typically rely on mocking URLSession in order to write unit tests for our network layer. Given that GraphQL and Apollo abstract our networking layer and act as a sort of black box for networking resources, we cannot rely on the typical URLSession mock strategy to achieve maximum unit testing coverage.

Let’s use a public GraphQL API to create an example application and write some unit tests. Our example app will be a simple table view master detail application which lists the countries of the wold and can load details about them on the detail page.

Initially, let’s think about how we’re going to structure our data. Consider that the master page only needs a few data points on a country, where the details page will require far more data about a specific country. This is one of the reasons to use GraphQL, we can actually define separate models for separate use cases.

This technique of only fetching what you need for a given screen can provide performance improvements for our application, decreasing our TTI (time to interaction) and improve the user experience.

The GraphQL Data Model

We will define a few GraphQL Fragments to design our model.

Next, let’s define our query to fetch a list of CountryLite models:

Now, we can consider our detail page. On this page, we will require more data about a given country than on the master table view. We can create that Fragment as follows:

And finally, we add the Query to fetch a single CountryDetail model.

Excellent. Now that we have our data models defined, let’s get into the actual networking service class implementation in Swift.

The Swift Data Model

Let’s begin by defining a namespace for our domain models and Store, which will act as our service class in our case. Initially, we will define structs to express our GraphQL data returned from Apollo with the queries above. This will ensure that we aren't allowing GraphQL to "leak" into our application any further than we need. Additionally, we will define an Error conforming enum to express failure states within our networking layer.

Why are all of these objects marked as public?

In my implementation, the data layer exists in a Cocoa Touch Framework, not the main application target. This is also why inits are required here, you cannot automatically synthesize a publicly accessible initializer with Swift struct.

Why do the data models conform to Hashable?

Considering we require Equatible conformance for testing equality within our unit tests, conformance to Hashable will acheve this, as well as providing a hashValue property should we require it for caching.

Dealing with Optionality within GraphQL data 💥

After defining our data model in Swift structs, we should create optional initalizers for these objects from the GraphQL models. This will make actually parsing our data and transforming it into our domain models within our service simple. Apollo makes everything optional 😡, so all of our initializers are failable. You might know a better way to remove optionality from GraphQL data, please post a comment if you do.

We will rely on flatMap and compactMap when it comes to transforming to deal with optionality.

The Swift Networking Service 📡

Let’s begin by defining a protocol for our Store's interface.

Next, let’s define a protocol describing only the functionality from Apollo which we need, called ApolloClientInterface. For now, this is only Queries, as we are not preforming any Mutations. From there, let's ensure the ApolloClient conforms to our protocol.

We will use the Result library to handle Success and Failure states for our network calls. This simplifies error handling, and returns Result<Value, Error> within an async completion block.

Now that we have our interfaces defined, lets create our actual service class.

Network Service Usage 🔧

First, we can add a factory to produce the World.Store objects.

At he usage point where we would like to call our service we will create the factory and initialize the store.

The result should contain the folowing data when you po result in the LLDB.

In part two, we will implement unit tests to gain ~98% test coverage on our store object through mocking Apollo.

Originally published at gist.github.com.

--

--