Networking in Swift part 2: Unit testing

Project code:

Level:

In this post we are going to add unit testing to the HTTP client that we created in the previous post.

In order to test the URLSession class, Apple provides us with a protocol called URLProtocol, which allows us to change the behavior of URLSession and choose what response it returns or even what error it throws when making a request.

We are going to create the class that implements URLProtocol to be able to mock URLSession requests.

class MockURLProtocol: URLProtocol {
    static var error: Error?
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
    
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override func startLoading() {
        if let error = MockURLProtocol.error {
            client?.urlProtocol(self, didFailWithError: error)
            return
        }
        
        guard let handler = MockURLProtocol.requestHandler else {
            assertionFailure("Received unexpected request with no handler set")
            return
        }
        
        do {
            let (response, data) = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }
    
    override func stopLoading() {
        // TODO: Andd stop loading here
    }
}

Let’s create a new class for HTTPClient unit tests and call it HTTPClientTests:

import XCTest
@testable import Network_Client_Example

final class HTTPClientTests: XCTestCase {
    
    private var sut: HTTPClient!
    
    override func setUp() {
        super.setUp()
        let session = createMockSession()
        sut = HTTPClient(session: session)
    }
    
    private func createMockSession() -> URLSession {
        let configuration: URLSessionConfiguration = .ephemeral
        configuration.protocolClasses = [MockURLProtocol.self]
        return URLSession(configuration: configuration)
    }
}

We define the system under test (sut) of type HTTPClient and we will create a new instance of this variable in each test, using the setUp method. When creating the URLSession, within the URLSessionConfiguration you can add the MockURLProtocol class to change the behavior of the URLSession.

Now we can create the first test, which validates that a request with a 200 response returns the correct data:

func test_Send_Request_200_Assert_Data_Is_Equal() async {
    // Given
    let json =
              """
              {
                 "name" : "Manel",
                 "age" : 28
              }
              """
    let data = json.data(using: .utf8)!
    let url = URL(string: "http://owr.cla.mybluehost.me/test")!
    
    MockURLProtocol.error = nil
    MockURLProtocol.requestHandler = { request in
        let response = HTTPURLResponse(url: url,
                                       statusCode: 200,
                                       httpVersion: nil,
                                       headerFields: ["Content-Type": "application/json"])!
        return (response, data)
    }
    
    // When
    let urlRequest = URLRequest(url: url)
    do {
        let result = try await self.sut.send(request: urlRequest)
        // Then
        XCTAssertEqual(result, data)
    } catch {
        XCTFail("This request should not fail")
    }
}

Once the first test is working, you can create all the necessary tests to be able to test all the behavior of the class. If you want you can see all the tests I have created in the github repository.

Thank you for reading this post!

Leave a Reply

Your email address will not be published. Required fields are marked *