Testing singletons

The singleton is one of the most controversial designing patterns. Its aim is to have one, and only one, instance of a particular class in our code base, but what happens is that we abuse the global nature of the singleton to have all the dependencies at hand when we need them. In this way, however, we are losing the power of DI as we have seen in Chapter 11, Implementing Dependency Injection.
Since a Singleton cannot be instantiated, it cannot be replaced with a test double. In his blog (https://www.swiftbysundell.com/), John Sundel demonstrates a few steps to make a singleton replaceable at testing time.

Let's try some code that relies on URLSession.instance to work:

class DataFetcher {
enum Result {
case data(Data)
case error(Error)
}

func fetch(from url: URL, completionHandler: @escaping (Result) -> Void) {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let error = error {
return completionHandler(.error(error))
}

completionHandler(.data(data ?? Data()))
}

task.resume()
}
}

If we want to mock URLSession to verify the calls from FetcherLoader, we cannot do it.
The first step is to define a protocol for the mock that defines the only function of URLSession that is used from the singleton:

protocol NetworkSession {
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void

func performRequest(for url: URL, completionHandler: @escaping CompletionHandler)
}

Then, implement this as a URLSession extension:

extension URLSession: NetworkSession {
typealias CompletionHandler = NetworkSession.CompletionHandler

func performRequest(for url: URL, completionHandler: @escaping CompletionHandler) {
let task = dataTask(with: url, completionHandler: completionHandler)
task.resume()
}
}

Having made URLSession conform to NetworkSession, we can inject it instead of using the shared global instance in the code. Also, the injected object is defaulted to URLSession.instance, so that the code that relies on FetcherLoader doesn't need to be changed:

class DateFetcher {
enum Result {
case data(Data)
case error(Error)
}
private let session: NetworkSession

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

func fetch(from url: URL, completionHandler: @escaping (Result) -> Void) {
session.performRequest(for: url) { (data, response, error) in
if let error = error {
return completionHandler(.error(error))
}

completionHandler(.data(data ?? Data()))
}
}
}

And now, we can finally write the test that injects a mock in a determinable way:

func testFetchingData() {
class MockNetworkSession: NetworkSession {
typealias CompletionHandler = NetworkSession.CompletionHandler

var requestedURL: URL?

func performRequest(for url: URL, completionHandler: @escaping CompletionHandler) {
requestedURL = url

let data = "Hello world".data(using: .utf8)
completionHandler(data, nil, nil)
}
}

let session = MockNetworkSession()
let loader = DataFetcher(session: session)

var result: DataFetcher.Result = .data("".data(using: .utf8)!)
let url = URL(string: "test/api")!
loader.fetch(from: url) { result = $0 }

XCTAssertEqual(session.requestedURL, url)
switch result {
case .data(let data):
XCTAssertEqual(data, "Hello world".data(using: .utf8)!)
case .error:
XCTFail()
}
}

One of the arguments against using singletons is that it makes the code difficult to test. This is certainly true, but we don't always have the freedom to get rid of singletons, maybe because they are provided by external libraries, such as in this case, or because they are developed by another team working on the same app.
However, using the technique we just experimented with, we can test the code thoroughly even with a lot of singletons in our code base.