Mocking Network Calls in Swift

Yes, you can test your code without making genuine network calls

When testing there are various reasons why you might not want to test against the real version of an API. This might be because you want to make sure that you are only testing one class or method of your code at once (so you can separate out different pieces of your testing regime), or (as in this case) you don’t want to make real network calls (because they might be flaky or you might not guarantee that the network connection is available in your testing environment.

Difficulty: Beginner | Easy | Normal | Challenging

Prerequisites:

  • Understand the difference between the terms subbing, mocking and faking (Guide HERE)
  • We will be using Swift’s result type (guide HERE) within this post
  • Be able to allow Arbitrary loads in your Plist settings (guide HERE)

Terminology

API: Application programming interface. A set of accessible tools for building software applications

Class: An object that defines properties and methods in common

Data Task: A task that retrieves the contents of a URL

Method: A function that defines the behaviour of object

Mocking: A fake response to the method call for of an object, allowing the checking of a particular method call or property

Result Type: A value that represents either a success or a failure, including an associated value in each case

The use of mocking

To be able to test network calls without actually hitting the remote server through thoseAPI calls is an invaluable skill to learn.

Look, putting load on a server just to test might seem like a silly thing to do because of the straight cost of doing so but it’s more than that. Waiting for a response from a remote API is slowwwwww and you are dependent on that API for your tests passing.

Step by Step Code allowing Testability

Downloading from a URL

In this case, we are downloading a 10MB file from http://ipv4.download.thinkbroadband.com/10MB.zip although any file URL you can think of would operate in the same way.

Theory: A data task to download

To download a file data task is a method on the URLSession class that allows us the download the contents of a URL.

Create a simple function to download data

This gives us a simple function, that accepts a parameter that is a URLSession(for easy use of dependency injection later).

Now I do sometimes forget that we need to task.resume() to start the download, but that’s my only complaint (and as such I’m criticising my own understanding).

Now to run this method we run the following:

downloadData(URLSession.shared)

To get the code to download (even in the simulator) you will need to adjust the App Transport Security Security settings (guide HERE).

Putting it together we can add the following to a view controller

It should be noted that this type of architecture violates the single responsibility principle and of course there are better ways to structure the architecture of your App. However this article is about testing.

Simple testing

We set the storyboardid for the view controller in the storyboard (there is a guide for initialising a view controller in Swift Guide), so the viewcontroller can be intialised.

It seems like there is no problem here. Now we can run our function in the viewcontroller.

You know what happens here. It just isn’t reliable even with the 10 seconds timeout — I can get a response around 1 in 3 times (that gives a passing test).

There must be a better way. Luckily, of course there is!

We shouldn’t be making the network call from this test at all. Even if the test above worked, it has to make the call each time, and this takes too long.

This code is contained within the repo towards the bottom of the page, and within that repo this code is in a NaiveMockNetworkCalls folder.

Mocking URLSession through different methods

We want to test our code without actually downloading anything, we need to set up a mock for URLSession .

Subclassing URLSession

Our first attempt at this mock class has two sections, that is URLSessionDataTaskMock and URLSessionMock. The URLSession itself is subclassed, and this means we can override the dataTask functionality.

The URLSessionDataTaskMock returns a closure it has been given when when resumed; that is when it is called it will simply return that closure.

The URLSessionMock is a class that returns that URLSessionDataTaskMock rather than the usual URLSessionDataTask (which itself is usually expected to return the downloaded data into the App’s memory).

As a result, when then call this with the following test, we inject some data (so there is something for URLSessionDataTaskMock to be able to return. The URLSessionMock() can be injected into the function downloadData as URLSessionMock() is actually a URLSession so no casting is required.

Now when the test is run, we don’t have to wait for the network to return — obviously it isn’t quite instant but on my machine I could change the timeout to 0.1 within testUsingSimpleMock.

This code is contained within the repo towards the bottom of the page, and within that repo this code is in a NaiveMockNetworkCalls folder.

The problem with subclassing

Subclassing URLSession and DataTask means that we are exposed to any changes that Apple might make to these classes, and (as you have seen) there is quite alot of code that is required for just some mocking.

Failed Attempt 1: Mock URLSession, conforming to a protocol and Mock DataTaskMock

Our second attempt at mocking here requires that we conform to a protocol.

protocol URLSessionProtocol {
func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}

So we need to extend URLSession to conform to the protocol

extension URLSession: URLSessionProtocol {}

Then we create the URLSession as before, however we need to mock the data task too where it conforms to the following URLSessionDataTaskProtocol

protocol URLSessionDataTaskProtocol {
func resume()
}

so URLSessionDataTaskMock is as follows:

Now the issue is that URLSessionDataTaskMock within the URLSessionProtocol doesn’t have the correct type - It needs to return a URLSessionDataTask and it can’t be cast as above — that is it always fails.

Method 2: Mock URLSession, subclass DataTask

One (working) solution is to mock URLSession but to subclass DataTask. This allows us to create a skinny DataTaskMock which basically overrides resume to with an empty function body.

This code is contained within the repo towards the bottom of the page, and within that repo this code is in a MockNetworkCalls folder.

Method 3: Mock URLSession, Mock DataTask

We can mock both URLSession and DataTask by using similar techniques to the one above.

Now we change the URLSessionProtocol to return an associated type

protocol URLSessionProtocol {
associatedtype dta: URLSessionDataTaskProtocol
func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> dta
}

where URLSessionDataTaskProtocol is as before

protocol URLSessionDataTaskProtocol {
func resume()
}

and the extensions are written the same as before above.

We have to use a generic constraint in our function signature as below

Our mock class is then represented by the following:

which is then tested with exactly the same test as before (full code is in the repo at the link just under this code):

class MockNetworkCallsTests: XCTestCase {
func testUsingSimpleMock() {
let mockSession = URLSessionMock()
mockSession.data = "testData".data(using: .ascii)
let exp = expectation(description: "Loading URL")
let vc = ViewController()
vc.downloadData(mockSession, completionBlock: {data in
exp.fulfill()
})
waitForExpectations(timeout: 0.1)
}
}

This code is contained within the repo towards the bottom of the page, and within that repo this code is in a FullMockNetworkCalls folder.

Repo

This is a rather long article, so please download the repo to find the code

https://github.com/stevencurtis/MockNetworkCalls

Conclusion:

Mocking prevents your tests committing the following sins:

  • Waiting for a slow API call
  • Hitting your remote API (at cost to the business)
  • Dependent on an external resource for your tests to pass

Networking being one essential use for mocking, I hope this guide has helped you out a little!

The Twitter contact:

Any questions? You can get in touch with me HERE

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store