Partial Mocking in Swift — Backwards

Eduard Levshteyn
ITNEXT
Published in
6 min readJun 21, 2018

--

Brief disclaimer: This post is for iOS developers who are beginning to learn Unit Testing. Or for experienced iOS developers who have never written tests before.

There’s a wonderful video tutorial series on Ray Wenderlich (the first video is free, but you’ll need a subscription to watch the rest). My experience with it so far went something like this:

My typical experience with a tutorial

The complexity ramped up quickly, as it does with all sufficiently important topics. I came across an excellent post by John Sundell entitled Mocking In Swift, which went through the process in more detail. I highly recommend reading that article first, as well as starting with the RW series on Unit Testing in order to get the most out of what is to follow.

So, what is to follow? I’ll step through the way I approached creating a partially mocked test using John Sundell’s strategy & code.

Creating a Test

First, let’s say you have some sort of networking class that fetches data asynchronously. Second, let’s assume that you know how to create an XCTestCase subclass for your unit tests. Naturally, it would be nice if we could test our networking class. So how do we go about doing this? Let’s look first at our networking class:

Networking class. Creates a data task and returns the result.
NetworkResult enumeration

What we have above is a loadData function that takes in a URL and an escaping closure with a NetworkResult input. We create a data task using the shared URLSession instance, which returns a closure with data and error objects. Then we set our enum, and throw it into the completion handler.

There are a lot of things that we might want to test! Does the networking call return the correct data? Can we connect correctly to the URL we pass? As Sundell writes, we could write a test that simply calls the loadData function with a working URL — but that technique has several pitfalls.

For one, the internet could be down and so our data task would fail. We could also get some junk data from our URL. But there’s a much simpler characteristic we should test first — if there is a genuine error, does our code return a failure case? And if there is some data returned, would we get a success case? As Sundell suggests, we could start writing our unit test for this case in the following manner:

In the above test, we create a URL as well as instance of our networking class. Then, we call loadData on our instance, and store the result in a local variable. Finally, we test that the result we get from our networking call is equal to a success case initialized with some data variable (which is not yet defined).

For our assert to work, we need data! And for that, we need a functioning URL, and an internet connection, and time (for the function to load async). This is where mocking comes in — rather than use a real URL to get real data, why not use a fake URL and get fake data?

Partial Mocking

First, we have to understand what’s happening in our assert case. We are comparing result, which is a NetworkResult, with .success(data). This test will pass if they both contain the same data object. It’s easy enough to create an instance of a fake Data object that we specify and fill our own NetworkResult enum. However — how do we make sure that our loadData function returns the same fake data?

To figure that out, look at the loadData code above. The data object is returned from a dataTask created with our URL and using a shared URLSession. In order to fool our test, we will have to somehow rewrite that function such that it returns our fake data. One way to do this is to create a subclass of URLSession and override the dataTask function to make it do what we want. This URLSession subclass must be used within the NetworkManager.

How can we make sure that our networking class uses URLSession.sharedby default, but still customize it in our test case to use our custom URLSession subclass? This is where dependency injection comes into play. This allows us to instantiate a NetworkManager object with the URLSession we create. We must also not forget to let that URLSession know about the fake data we created in the test case.

Let’s summarize all that. First, we create a fake URL. Then we create fake data. Then we instantiate our URLSession subclass, and send it the fake data we created. Then we create a NetworkManager instance and initialize it with our URLSession subclass. How does that look in a test case? Maybe something like this:

Now we work our way up the tree. The first stop is NetworkManager, where we need to add some code that will allow us to give it a URLSession on initialization (see line 15 above), but also keep the original functionality of using URLSession.shared by default. To do this, we know we need a property of type URLSession that will store the session to be used, and we also need to create our own initializer which will allow us to write code like NetworkManager(session: session) but also keep a default value. Thankfully, Swift provides for us this functionality with default parameter values:

You can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.

Let’s take a look at our new Network Manager code:

Compare to the earlier code and you can see there are only slight modifications as stated above. Rather than using URLSession.shared we now use our private session property.

Great! We have our test code, our networking code. All that’s left is to create our URLSession subclass.

In the NetworkManager code, we can see that our subclass must have a data property (since we set session.data), and we know it must override the dataTask function. Thinking somewhat ahead, we can also assume it must have an error property since we would like to eventually test an error state. Looking at our loadData function, we also know that dataTask must take in an @escaping completion, since there is a callback. That completion has to return a URLSessionDataTask object, since it should be stored in the task variable in NetworkManager. The code so far:

So, what do we put inside of our dataTask function? Well, we have to return a DataTask object with our data inside of its completion. Since we have to customize DataTask, it sounds like we need another subclass. In our URLSessionMock subclass, then, we have to instantiate a mock DataTask object and return it. This DataTaskobject should have the property that upon calling its resume() function, the closure is finally called. It can look like the following:

Whew. Almost done. Now we get an error that our NetworkResult is not Equatable, yet we’re trying to equate it in our XCTAssert. That makes sense — so here’s a roughly Equatable version of NetworkResult. Not saying it’s perfect, but it works (see here for reference):

We just compare the underlying Data and Error objects.

The chain of completion handlers looks like this: In our test function, we send our first completion { result = $0 } to the loadData function. The loadData function takes an escaping (NetworkResult) -> Void completion, so $0 will come to represent NetworkResult. This completion works its way through URLSessionMock and finally URLSessionDataTaskMock until its called in the resume() function.

We run the test and it’s green! (Or at least, it should be).

A successful fake networking test

Whew! That was a lot of code for one test.

The above strategy is called Partial Mocking — whereby objects are partially modified in order to change their behavior.

Working backwards in this fashion is similar to Red Green Refactoring whereby a failing test is written first and then production code is written. In our case, we started even before this step by writing a test that didn’t compile until we created the necessary test classes.

John Sundell continues with an explanation of Complete Mocking as well as when Mocking should be avoided. The strategy above works just as well for Complete Mocking as it does for Partial Mocking!

Thanks for reading! If you have comments, corrections or questions, leave them below and I’ll do my best to answer — or, you can message me on Twitter-@eduardlev

--

--