Testing succeed notification Swift

Mocking Alamofire

When using third party libraries, it can be challenging writing test code to allow mocking. Especially for code that includes networking, you do not want to test your network connection or your backend service. You want to test your logic around the networking. This blog post will show you how to enable mocking for the network library Alamofire.

Fun fact: You can use the same approach to mock URLSession from the foundation framework.

Alamofire
An example API request would probably look like this:

import Foundation
import Alamofire

final class AuthenticationAPI {
    
    func login(username: String, password: String, completion: @escaping (DefaultDataResponse) -> Void) {
        let parameters: Parameters = ["username": username, "password": password]
        
        Alamofire.request("https://example.com/login", method: .post, parameters: parameters, encoding: JSONEncoding.default)
            .response { (response) in
                // hande response
            }
    }
}

To enable mocking Alamofire, you first have to look into the framework to see if it is injectable, so you can reroute the network response to a local response. By default, Alamofire will use the default SessionManager object to make request:

/// A default instance of `SessionManager`, used by top-level Alamofire request methods, and suitable for use
/// directly for any ad hoc requests.
public static let `default`: SessionManager = {
    let configuration = URLSessionConfiguration.default
    configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
    
    return SessionManager(configuration: configuration)
}()

Alamofire uses a URLSessionConfiguratio in its manager to make requests. Why do I show this you might ask? Well,  the configuration is injectable by its property protocolClasses,  an array of AnyClass. What this does is explained in the Apple docs:

The objects in this array are Class objects corresponding to custom URLProtocol subclasses that you define. URL session objects support a number of common networking protocols by default. Use this array to extend the default set of common networking protocols available for use by a session with one or more custom protocols that you define.

Prior to handling a request, the URLSession object searches the default protocols first and then checks your custom protocols until it finds one capable of handling the specified request. It uses the protocol whose canInit(with:) class method returns true, indicating that the class is capable of handling the specified request.

In other words; when making a request, if our given protocol returns true for the specified request, we can modify the response!  But how does this look like in code? Let us try it out!

First we have to create our own URLProtocol class, I would recommend adding this class to your test target because you do not want to accidentally use this class in production. If we look at the methods in URLProtocol, you will notice that we must implement canInit and canonicalRequest. We will also implement requestIsCacheEquivalent, startLoading and stopLoading.

import Foundation

final class MockURLProtocol: URLProtocol {
    
    private(set) var activeTask: URLSessionTask?
    
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool {
        return false
    }
    
    override func startLoading() {
        activeTask?.cancel() // We don’t want to make a network request, we want to return our stubbed data ASAP
    }
    
    override func stopLoading() {
        activeTask?.cancel()
    }
}

As you noticed, we also have an activeTask property. We need this to store our task that we will create in startLoading. To create a URLSessionTask containing our API request, we need a URLSession property. If you look at the initialiser from URLSession, we have the following initialisers:

public /*not inherited*/ init(configuration: URLSessionConfiguration)

public /*not inherited*/ init(configuration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue queue: OperationQueue?)

We will be using the second initialiser so we can return our own mocked responses. First we will have to add the URLSessionDelegate to our MockURLProtocol class:

// MARK: - URLSessionDataDelegate
extension MockURLProtocol: URLSessionDataDelegate {
    
    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        client?.urlProtocol(self, didLoad: data)
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        client?.urlProtocolDidFinishLoading(self)
    }
}

This is where the magic will happen, when we create a URLSessionTask in startLoading, we will be cancelling our request right after and land in the didCompleteWithError delegate function. Here we can return any response we want! How? Add the following properties in MockURLProtocol:

enum ResponseType {
    case error(Error)
    case success(HTTPURLResponse)
}
static var responseType: ResponseType!

You can set the responseType anywhere you want! For example, I have added the following methods to set our mocking data:

extension MockURLProtocol {
    
    enum MockError: Error {
        case none
    }
    
    static func responseWithFailure() {
        MockURLProtocol.responseType = MockURLProtocol.ResponseType.error(MockError.none)
    }
    
    static func responseWithStatusCode(code: Int) {
        MockURLProtocol.responseType = MockURLProtocol.ResponseType.success(HTTPURLResponse(url: URL(string: "http://any.com")!, statusCode: code, httpVersion: nil, headerFields: nil)!)
    }
}

Using these methods we can give a default response with a given status code, or give a default error response. We have to modify our didCompleteWithError function, so it will return our mocked response. Replace didCompleteWithError with the following code:

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    switch MockURLProtocol.responseType {
    case .error(let error)?:
        client?.urlProtocol(self, didFailWithError: error)
    case .success(let response)?:
        client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
    default:
        break
    }
    
    client?.urlProtocolDidFinishLoading(self)
}

What ever the value is, if it is set, we will fail or request with the given error or succeed the request with the given response. Even if the original request did fail or succeed, we are returning our stored response from our responseType property. Last thing we have to do is create our URLSessionTask in startLoading:

private lazy var session: URLSession = {
    let configuration: URLSessionConfiguration = URLSessionConfiguration.ephemeral
    return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}()

override func startLoading() {
    activeTask = session.dataTask(with: request.urlRequest!)
    activeTask?.cancel()
}

The essential part is activeTask?.cancel() if you do not want the API to continue, this way the request will fail immediately.

Okay, so we written our MockURLProtocol. How can we use this in our AuthenticationAPI? Well, we first have to enable dependency injection so we can add our MockURLProtocol to our class. In this example we will be using construction dependency injection. Let us rewrite the AuthenticationAPI to the following:

import Foundation
import Alamofire

final class AuthenticationAPI {
    
    private let manager: SessionManager
    init(manager: SessionManager = SessionManager.default) {
        self.manager = manager
    }
    
    func login(username: String, password: String, completion: @escaping (DefaultDataResponse) -> Void) {
        let parameters: Parameters = ["username": username, "password": password]
        
        manager.request("https://example.com/login", method: .post, parameters: parameters, encoding: JSONEncoding.default)
            .response { (response) in
                // Handle repsonse
            }
    }
}

Notice the difference? We only added a initialiser with a default value of the default Alamofire SessionManager. This way our app does not need to change anything and will still use the default behaviour it had before. Now we can give our custom SessionManager with the MockURLProtocol for our tests. How? Well, let us write a unit test for our AuthenticationAPI:

import XCTest
@testable import Alamofire

final class AuthenticationAPITest: XCTestCase {
    
    private var sut: AuthenticationAPI!
    
    override func setUp() {
        super.setUp()
        
        let manager: SessionManager = {
            let configuration: URLSessionConfiguration = {
                let configuration = URLSessionConfiguration.default
                configuration.protocolClasses = [MockURLProtocol.self]
                return configuration
            }()
            
            return SessionManager(configuration: configuration)
        }()
        sut = AuthenticationAPI(manager: manager)
    }
    
    override func tearDown() {
        super.tearDown()
        
        sut = nil
    }
    
    func testStatusCode200ReturnsStatusCode200() {
        // given
        MockURLProtocol.responseWithStatusCode(code: 200)
        
        let expectation = XCTestExpectation(description: "Performs a request")
        
        // when
        sut.login(username: "username", password: "password") { (result) in
            XCTAssertEqual(result.response?.statusCode, 200)
            expectation.fulfill()
        }
        
        // then
        wait(for: [expectation], timeout: 3)
    }
}

In our unit test preparation, we have created a SessionManager and added our MockURLProtocol to the protocolClasses in URLSessionConfiguration. This manager is then injected using the constructor from AuthenticationAPI and we save a reference to the AuthenticationAPI in the variable sut.

The unit test testStatusCode200ReturnsStatusCode200 we first call our static method to save our desired response. Then we call our login method from AuthenticationAPI and validate if the response status code is equal to 200. We add an expectation with a small timeout of 3 seconds because our API request is asynchronous. If we run the test, you will notice our test succeeds!

I hope this blog post enabled you to start writing your own mocked responses for your unit tests using Alamofire, URLSession or anything else! The MockURLProtocol class can be found on: https://github.com/Jeroenbb94/mocking-networkrequests