Mocking in Swift

Mocking is a key technique when it comes to writing unit tests in pretty much any language. When mocking an object, we are essentially creating a "fake" version of it - with the same API as the real one - in order to more easily be able to assert and verify outcomes in our test cases.

Whether we're testing networking code, code relying on hardware sensors like the accelerometer, or code using system APIs like location services - mocking can enable us to write tests a lot easier, and run them faster in a more predictable way.

However, there are also cases where mocking might not be necessary, and where including real objects in our tests can let us write tests that run under more realistic conditions. This week, let's take a look at a few different situations and how mocking can be used - or avoided - to make our tests easier to write, read and run.

Why mock something?

So first things first, let's take a look at an example that shows how mocking can be really useful. Let's say we're building a NetworkManager that lets us load data from a given URL:

class NetworkManager {
    func loadData(from url: URL,
                  completionHandler: @escaping (NetworkResult) -> Void) {
        let task = URLSession.shared.dataTask(with: url) { data, _, error in
            // Create either a .success or .failure case of a result enum
            let result = data.map(NetworkResult.success) ?? .failure(error)
            completionHandler(result)
        }

        task.resume()
    }
}

Now we want to write tests that verifies that .success and .error are returned in the correct situations. To do that, we could just simply call our loadData API and wait for a result to be returned, but that both requires our tests to be run with an Internet connection and will make them much slower to run (since we'll have to wait for a real request to be performed).

Instead, let's use mocking. What we'll want to do here is to make NetworkManager use a fake session in our test code, that doesn't actually perform any requests over the network, but instead lets us control exactly how the network will behave.

Partial mocking

Mocking comes in two different flavors - partial and complete. When doing partial mocking, you are modifying an existing type to only partially behave differently in a test, while when doing complete mocking you are replacing the entire implementation.

If we wanted to partially mock URLSession and the URLSessionDataTask that it returns, we'd create our mocks as subclasses that each override the methods we are expecting to be called, in order to return specific results that we can control in our tests. Let's start by creating a mocked data task that simply runs a closure when resumed:

// We create a partial mock by subclassing the original class
class URLSessionDataTaskMock: URLSessionDataTask {
    private let closure: () -> Void

    init(closure: @escaping () -> Void) {
        self.closure = closure
    }

    // We override the 'resume' method and simply call our closure
    // instead of actually resuming any task.
    override func resume() {
        closure()
    }
}

Now let's do the same thing to URLSession, but this time we'll override the dataTask method in order to return an instance of our mock class, like this:

class URLSessionMock: URLSession {
    typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void

    // Properties that enable us to set exactly what data or error
    // we want our mocked URLSession to return for any request.
    var data: Data?
    var error: Error?

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

        return URLSessionDataTaskMock {
            completionHandler(data, nil, error)
        }
    }
}

OK, our mocks are ready! Now to actually be able to use them, we need to add dependency injection to NetworkManager in order to let us inject a mocked session instead of always using URLSession.shared:

class NetworkManager {
    private let session: URLSession

    // By using a default argument (in this case .shared) we can add dependency
    // injection without making our app code more complicated.
    init(session: URLSession = .shared) {
        self.session = session
    }

    func loadData(from url: URL,
                  completionHandler: @escaping (NetworkResult) -> Void) {
        let task = session.dataTask(with: url) { data, _, error in
            let result = data.map(NetworkResult.success) ?? .failure(error)
            completionHandler(result)
        }

        task.resume()
    }
}

For more information about dependency injection, check out "Different flavors of dependency injection in Swift".

Finally, let's write our first test, that verifies that a successful result is returned if Data was returned from the network request:

class NetworkManagerTests: XCTestCase {
    func testSuccessfulResponse() {
        // Setup our objects
        let session = URLSessionMock()
        let manager = NetworkManager(session: session)

        // Create data and tell the session to always return it
        let data = Data(bytes: [0, 1, 0, 1])
        session.data = data

        // Create a URL (using the file path API to avoid optionals)
        let url = URL(fileURLWithPath: "url")

        // Perform the request and verify the result
        var result: NetworkResult?
        manager.loadData(from: url) { result = $0 }
        XCTAssertEqual(result, .success(data))
    }
}

We now have a test that verifies that our NetworkManager works as intended for successful responses πŸ‘! That's cool, but there's a lot of room for improvement. Using partial mocking, like we did above, can be useful at times - but it has two major downsides:

  1. It requires us to write quite a lot of mocking code, since we need to actively override all the code paths that we are expecting to be called.
  2. We are only partially modifying objects. That means that we are making some pretty hard assumptions both about how the objects work internally (we pretty much assume that the APIs we override aren't used internally by the object itself), and how we'll use them in our own code. These type of assumptions can quickly lead to flaky tests and false positives as the objects we are mocking change - especially when it comes to system classes like URLSession.

Complete mocking

Let's instead use complete mocking, which means that we'll replace the entire URLSession class with a completely mocked implementation. To do that, we can't subclass URLSession like we did when creating our partial mock, and instead we'll abstract the APIs we need from it into a protocol:

protocol NetworkSession {
    func loadData(from url: URL,
                  completionHandler: @escaping (Data?, Error?) -> Void)
}

Which we then make URLSession conform to by using an extension:

extension URLSession: NetworkSession {
    func loadData(from url: URL,
                  completionHandler: @escaping (Data?, Error?) -> Void) {
        let task = dataTask(with: url) { (data, _, error) in
            completionHandler(data, error)
        }

        task.resume()
    }
}

Finally, we'll make NetworkManager accept an object conforming to NetworkSession in its initializer, instead of a URLSession instance:

class NetworkManager {
    private let session: NetworkSession

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

    func loadData(from url: URL,
                  completionHandler: @escaping (NetworkResult) -> Void) {
        session.loadData(from: url) { data, error in
            let result = data.map(NetworkResult.success) ?? .failure(error)
            completionHandler(result)
        }
    }
}

You might recognize the above technique from earlier posts, like "Building an enum-based analytics system in Swift", in which we created an AnalyticsEngine protocol that also enabled us to have a specific implementation for our tests.

The big benefit of using complete mocking, is that we can now much easier create a mock for our tests, by simply implementing the NetworkSession protocol:

class NetworkSessionMock: NetworkSession {
    var data: Data?
    var error: Error?

    func loadData(from url: URL,
                  completionHandler: @escaping (Data?, Error?) -> Void) {
        completionHandler(data, error)
    }
}

No assumptions are made about the internals of URLSession, and we now have a much more stronger API contract between NetworkManager and its underlying session as well πŸ‘.

One thing to keep in mind when using complete mocking is to try to keep the protocols as thin as possible, otherwise you'll end up having to implement lots of methods and functionality in your mocks. Ideally, mocks should be super simple and only contain little to no logic at all. One way to do this is to break up & compose protocols as much as possible, like we took a look at in "Separation of concerns using protocols in Swift".

Avoiding mocking

Now that we've taken a look at various ways to implement mocks in Swift, let's take a look at an example in which we'd actually like to avoid mocking. When getting used to a technique such as mocking, it can sometimes be easy to start treating it like a silver bullet, and always use it in every situation. While most tests really benefit from mocking and makes it easier to test a given class in isolation, it's not always necessary.

Let's say we're building a FileLoader that lets us load files from the file system. To do that, we need to resolve a file system URL for a given file name, and to do that we'll use the app's Bundle. The Bundle API is similar to the URLSession API we used earlier, in that it's singleton-based and normally used by accessing its shared instance - in this case .main. So our initial idea might be to do the exact same thing as we did with URLSession - create a protocol for it that we then mock.

However, when it comes to Bundle, that's really not necessary and can actually make our testing code more complicated than it needs to be. What we can do instead, is to simply use our test suite's bundle, and include any files that we'd like to load in that bundle instead. In Xcode, we can create a file - let's call it TestFile.txt - and add it to our test target. Then, we'll tell our FileLoader to use our test bundle by giving Bundle our test case class in its initializer, like this:

class FileLoaderTests: XCTestCase {
    func testReadingFileAsString() throws {
        // By passing our test case class into Bundle's initializer,
        // we make the system use our test bundle instead of our app's bundle.
        let bundle = Bundle(for: type(of: self))
        let loader = FileLoader(bundle: bundle)

        let string = try loader.stringFromFile(named: "TestFile.txt")
        XCTAssertEqual(string, "I'm a test file!\n")
    }
}

So mocks are not always required, and if we can get away with avoiding them (and still write nice & stable tests), it can sometimes make testing code a lot simpler! πŸŽ‰

Conclusion

To mock or not to mock, that's the question πŸ˜…. I hope this post has given you some insight into how I work with mocks and how to apply various mocking techniques in Swift. Like always, my recommendation is to learn about the various techniques that we have at our disposal, and then apply them wherever you feel is the most appropriate. Using partial, complete and no mocking each has use cases and tradeoffs.

Another technique that can potentially be super useful when working with mocks is code generation. While I personally mostly write my mocks by hand in order to always have to make a conscious decision about why and how I'd like to mock a given object - code generation can really help speed up the mocking process in many different situations. We'll take a closer look at various code generation tools & techniques in an upcoming post.

What do you think? Do you have a preferred way of working with mocks in your tests, or will you try out one of the techniques from this posts? Let me know, along with any questions, comments or feedback you have - either in the comments section below or on Twitter @johnsundell.

Thanks for reading! πŸš€

Transforming collections in Swift

First class functions in Swift