Testing Swift code that uses system singletons in 3 easy steps

Most apps written for any of Apple’s platforms rely on APIs that are singleton based. From UIScreen to UIApplication to NSBundle, static APIs are everywhere in Foundation, UIKit & AppKit.

While singletons are very convenient and give easy access to a certain API from anywhere, they also pose a challenge when it comes to code decoupling and testing. Singletons are also a quite common source of bugs, where state ends up being shared and mutations not propagated properly throughout the system.

However, while we can refactor our own code to only use singletons where really needed, we can’t do much about what the system APIs give us. But the good news is, there are some techniques you can use to make your code that uses system singletons still be easy to manage & easy to test.

Let’s have a look at some code that uses the URLSession.shared singleton:

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

    func load(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()
    }
}

The above DataLoader is currently very hard to test, as it will automatically call the shared URL session and perform a network call. This would require us to add waiting and timeouts to our testing code, and it quickly becomes very tricky and unstable.

Instead, let’s go through 3 easy steps to make this code still as simple to use as currently, but making it a lot easier to test.

1. Abstract into a protocol

Our first task is to move the parts from URLSession that we need into a protocol that we can then easily mock in our tests. In my talk “Writing Swift code with great testability” I recommend avoiding mocks when possible, and while that’s a good strategy to follow for your own code, when interacting with system singletons — mocking becomes an essential tool to increase predictability.

Let’s create a NetworkEngine protocol and make URLSession conform to it:

protocol NetworkEngine {
    typealias Handler = (Data?, URLResponse?, Error?) -> Void

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

extension URLSession: NetworkEngine {
    typealias Handler = NetworkEngine.Handler

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

As you can see above, we let URLSessionDataTask be an implementation detail of URLSession. That way, we avoid having to create multiple mocks in our tests, and can focus on the NetworkEngine API.

2. Use the protocol with the singleton as the default

Now, let’s update our DataLoader from before to use the new NetworkEngine protocol, and get it injected as a dependency. We’ll use URLSession.shared as the default argument, so that we can maintain backward compatibility and the same convenience as we had before.

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

    private let engine: NetworkEngine

    init(engine: NetworkEngine = URLSession.shared) {
        self.engine = engine
    }

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

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

By using a default argument, we can still easily create a DataLoader without having to supply a NetworkEngine — simply using DataLoader() — just like before.

3. Mock the protocol in your tests

Finally, let’s write a test — where we’ll mock NetworkEngine to make our test fast, predictable and easy to maintain.

func testLoadingData() {
    class NetworkEngineMock: NetworkEngine {
        typealias Handler = NetworkEngine.Handler 

        var requestedURL: URL?

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

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

    let engine = NetworkEngineMock()
    let loader = DataLoader(engine: engine)

    var result: DataLoader.Result?
    let url = URL(string: “my/API”)!
    loader.load(from: url) { result = $0 }

    XCTAssertEqual(engine.requestedURL, url)
    XCTAssertEqual(result, .data(“Hello world”.data(using: .utf8)!))
}

Above you can see that I try to keep my mock as simple as possible. Instead of creating complicated mocks with lots of logic, it’s usually a good idea to just have them return some hardcoded value, that you can then make asserts against in your test. Otherwise, the risk is that you end up testing your mock more than you’re actually testing your production code.

That’s it!

We now have testable code, that still uses a system singleton for convenience — all by following these 3 easy steps:

1. Abstract into a protocol

2. Use the protocol with the singleton as the default

3. Mock the protocol in your tests

Feel free to reach out to me on Twitter if you have any questions, suggestions or feedback. I’d also love to hear from you if you have any topic that you’d like me to cover in an upcoming post.

Thanks for reading 🚀

Namespacing Swift code with nested types

Simple Swift dependency injection with functions