How to mock API in Swift

Tonny
4 min readFeb 24, 2022

When we write unit tests or UI tests for applications, sending the actual API request to hit the backend is a waste and it perhaps pollutes the database as repetitive tests run, it also introduces extra complexity as concurrent threads get involved. In this story, I’ll list four different mocking solutions from basic to advance to this common problem. All the solutions can be adopted in any sized project from small to large.

To demonstrate the API scenario, here is a simplified API client, it has an initializer injection for the session and one endpoint to fetch user info by id.

class APIClient {
let session: URLSession

init(session: URLSession = URLSession.shared) {
self.session = session
}

func getUserBy(
id: Int,
completionHandler: @escaping CompletionHandler
) {
let url = URL(string: "\(HOST)/api/user/\(id)")! session.dataTask(with: url, completionHandler: completionHandler)
}
}
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void

1. Guard

The first solution works like a guard, it checks the mocking data existence and invokes the completion handler right before the request sending out. It’s straightforward and sort of brute-force. To mock API in unit testing or UI testing, we just need to pass the mocking data to the client.

class APIClient {
...
var mockingData: Data?

func getUserBy(
id: Int,
completionHandler: @escaping CompletionHandler
) {
if let mockingData = mockingData { completionHandler(mockingData, nil, nil) return
}
let url = URL(string: "\(HOST)/api/user/\(id)")!
...
}
}

Summary: It’s simple, but it needs to modify the API client or other request context with the mocking data. It’s destructive to existing code.

2. Customised URLSession

The API client’s initialiser accepts any instance in URLSesssion type, so we can use the subclass of URLSession to mock it. And let’s override the networking methods to return the mocking data rather than send requests out.

class MockingURLSession: URLSession {
var data: Data?
var error: Error?

override func dataTask(
with url: URL,
completionHandler: @escaping CompletionHandler
) -> URLSessionDataTask {
completionHandler(self.data, nil, self.error)

return URLSessionDataTask()
}
}

Here is the usage of the subclass. The mocking session is created with mocking data, and then it’s injected into the API client in unit tests or UI tests. The mocking logic is extracted into the subclass and the API client is not aware of it.

if isUnitTesting || isUITesting {    let mockingSession = MockingURLSession()
mockingSession.data = "{\"id\": 1}".data(using: .utf8)
client = APIClient(session: mockingSession)} else { client = APIClient()
}
client.getUserBy(id: 1) { data, response, error in print(String(data: data!, encoding: .utf8)!) //{"id": 1}
}

You probably find the override method returns a URLSessionDataTask instance but it’s never been used, and it does not support task resume either. Here is a polished variant.

class MockingURLSessionDataTask: URLSessionDataTask {
private let closure: () -> Void
init(closure: @escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class MockingURLSession: URLSession {
var data: Data?
var error: Error?

override func dataTask(
with url: URL,
completionHandler: @escaping CompletionHandler
) -> URLSessionDataTask {
return MockingURLSessionDataTask {
completionHandler(self.data, nil, self.error)
}
}
}

Summary: the subclass way works, but the injected session type in the initialiser is not an abstract type, it’s not a good practice. And URLSession init() method was deprecated in iOS 13.0, Xcode will warn you when creating an instance of MockingURLSession, but it’s totally safe to ignore it.

3. URLSession sibling

The injected type in the initialiser can be lifted up as a protocol. This is a best practice of dependency injection. So the MockingURLSession can be a sibling of URLSession.

protocol URLSessionProtocol {

func dataTask(
with url: URL,
completionHandler: @escaping CompletionHandler
) -> URLSessionDataTask
}
extension URLSession: URLSessionProtocol {}class APIClient {
let session: URLSessionProtocol

init(session: URLSessionProtocol = URLSession.shared) {
self.session = session
}

...
}
class MockingURLSession: URLSessionProtocol {
...
}

Summary: The lifting helps the structure to match S.O.L.I.D., but it does not solve the `init() method was deprecated in iOS 13.0` issue.

4. Customised URLProtocol

Customised URLProtocol is an interceptor of request, it can read the information from the request before it’s sent out, and it also can respond a mocking data for the request internally.

Don’t instantiate a URLProtocol subclass directly. Instead, create subclasses for any custom protocols or URL schemes that your app supports. When a download starts, the system creates the appropriate protocol object to handle the corresponding URL request. You define your protocol class and call the registerClass(_:) class method during your app’s launch time so that the system is aware of your protocol.
— Apple document

class MockingURLProtocol: URLProtocol {
static var data: Data?
static var error: Error?

override class func canInit(with request: URLRequest) -> Bool {
true
}

override class func canonicalRequest(for request: URLRequest) -> URLRequest {
request
}

override func startLoading() {
if let data = Self.data {
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} else if let error = Self.error {
client?.urlProtocol(self, didFailWithError: error)
}
}

override func stopLoading() {}
}

After setting mocking data and registering the URLProtocol, it starts listening to all URLSession tasks now.

if isUnitTesting || isUITesting {
MockingURLProtocol.data = "{\"id\": 1}".data(using: .utf8)
URLProtocol.registerClass(MockingURLProtocol.self)
}

The API client and URLSession are not aware of the mocking details at all.

let client = APIClient()client.getUserBy(id: 1) { data, response, error in

print(String(data: data!, encoding: .utf8)!) //{"id": 1}
}

This example simply holds the mocking data by a static property, but in a real project, its mocking data is usually configured in JSON files. In the MockingURLProtocol, we can read these files depending on the information of request, parse data, and then respond accordingly.

Summary: The registered URLProtocol does not affect the existing network code, and it provides a central place to manage all mockings. I would love to use it.🙌

How do you mock API in your projects?

--

--