The guide to unit testing in Swift with Apollo and GraphQL Part One
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 init
s 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.