Articles, podcasts and news about Swift development, by John Sundell.

Mock-free unit tests in Swift

Published on 02 Dec 2018
Discover page available: Unit Testing

When getting started with unit testing, it usually doesn't take long to realize that some form of mocking is needed. When using mocking, we use various techniques to create "fake" versions of the objects that the functionality we want to test depends on - making it possible to verify the outcome of our code without relying on external logic or things like networking or database calls.

In "Mocking in Swift" we took a look at how a few common mocking techniques can be used in Swift, and although mocking will most likely remain essential for many types of testing, there are also many cases where avoiding mocks can lead to much simpler code - both in terms of testing and actual production code.

This week, let's take a look at some of those cases, and a few different ways to write mock-free unit tests in Swift.

Protocols, protocols everywhere!

One common complaint when refactoring code for testability is that it usually leads to our code base becoming full of protocols that, testing aside, wouldn't really need to be there. For example, let's say that we want to write tests for a class that uses a cache in order to speed up repeated operations. A common thing to do in such a situation is to create a CacheProtocol that we then make our production class Cache conform to, like this:

protocol CacheProtocol {
    associatedtype Value

    func cache(_ value: Value, key: String)
    func value(for key: String) -> Value?
}

extension Cache: CacheProtocol {}

The benefit of the above approach is that we can now make all objects that depend on Cache instead use the CacheProtocol API, which both leads to more decoupled code, and lets us mock it in our tests - such as here where we're writing tests for a class that converts articles into HTML:

class ArticleConverterTests: XCTestCase {
    func testCachingConvertedHTMLArticle() {
        let cache = CacheMock()
        let converter = ArticleConverter(cache: cache)
        ...
    }
}

While introducing protocol-based abstractions, like we do above, can be a great way to improve the separation of concerns in our code base - and for some (especially more "heavy") objects it can be a great approach - it does add some overhead and more code to maintain, especially if used for many objects throughout our code base. It also has the downside of separating our test code from our production code, which can sometimes lead to tests that are more verifying mocking code than actual production code.

Keeping it real

One alternative to using mocks is to actually use our real objects in our tests as well. While not always possible, there are many cases in which mocks are simply not needed - and instantiating a real object instead can both let us get rid of protocols that are only there to facilitate testing, and makes our tests run under more realistic conditions as well.

Going back to our ArticleConverterTests example from before, let's see what a test could look like if we were to simply use our real Cache implementation instead of using a mock. What we'll do is that we'll create a blank instance of our cache, inject that into our ArticleConverter, and then use our real caching APIs to verify that the right conditions are met - like this:

class ArticleConverterTests: XCTestCase {
    func testCachingConvertedHTMLArticle() {
        let cache = Cache<String>()
        let converter = ArticleConverter(cache: cache)
        let article = Article(title: "Title", text: "Text")

        // It's often a good idea to verify our assumptions when
        // writing tests, such as checking that the cache doesn't
        // actually contain any value *before* we've run our code.
        XCTAssertNil(cache.value(for: article.id))

        let html = converter.convert(article, to: .html)
        XCTAssertFalse(html.isEmpty)

        let cachedHTML = cache.value(for: article.id)
        XCTAssertEqual(cachedHTML, html)
    }
}

While some people might argue that we've now turned our unit test into an integration test (since it actually integrates our Cache class into ArticleConverter) - the question is, if we can simplify our test code, reduce the need for extra protocols and mocks - does it really matter whether our test is a "pure" unit test or not?

Temporary persistence

The above example of using a real Cache instance works really well - but only as long as our cache doesn't rely on any form of persistence, since we'll otherwise end up with failing tests due to old data still being around. For example, we might realize that our cache needs to write its entries onto disk as well - which will make our previous test start to fail.

However, before we jump back onto the mocking train, let's see if we can come up with a solution that'll still let us use our real Cache class, while also removing the risk of flakiness and failing tests. When it comes to persistence in particular, one way of solving our problem is to open Cache up to be configured with a specific file path to use when reading and writing to disk:

class Cache<Value> {
    private let filePath: String

    init(filePath: String) {
        self.filePath = filePath
    }
}

Just like when using mocking, this enables us to take control of some of the inner workings of Cache in our test code, but in a much more lightweight manner - without requiring additional types to be introduced. All we now have to do to make our tests much more predictable is to simply point our cache to a file path that we can guarantee won't contain any old state from previous test runs.

To be able to do that in a simple way, let's create a function that enables us to get a temporary file path that we make sure doesn't yet contain any data. We'll use NSTemporaryDirectory to get access to a directory appropriate for storing temporary data, and the #function compiler directive to automatically pass in the name of the test in which our function is being used - like this:

func makeTemporaryFilePathForTest(
    named testName: StaticString = #function
) -> String {
    let path = NSTemporaryDirectory() + "\(testName)"
    try? FileManager.default.removeItem(atPath: path)
    return path
}

We're now able to still use our real Cache class, but with a specific file path obtained by calling makeTemporaryFilePathForTest, which'll make our test continue to execute predictably:

class ArticleConverterTests: XCTestCase {
    func testCachingConvertedHTMLArticle() {
        let filePath = makeTemporaryFilePathForTest()
        let cache = Cache<String>(filePath: filePath)
        ...
    }
}

Just like how we used a temporary file path above, we can do something along the same lines to avoid having to introduce mocks for many other kinds of tests as well. For example, we can create a UserDefaults instance that's under our control by using a specific suiteName, we can use our test bundle for code dealing with the Bundle API, and we can use a specific instance of URLSession for networking.

Functional behavior

Sometimes the object we wish to test relies on some form of asynchronous behavior, like performing a network request. This is another area where mocking is incredibly popular - we'll simply create a mocked version of our networking class, which we can use to turn our async networking code synchronous, and make our tests both faster and more predictable.

Again, for more sophisticated networking code, that's a great solution - but what if we only need to perform a single request, do we really need to introduce mocking just for that?

Let's take a look at another example, in which we're building a SettingsManager that enables the user to enable and disable various settings in our app. Every time a setting is changed our manager performs a network call to propagate that setting to our server, using a static Networking API - like this:

class SettingsManager {
    private var settings: [Setting : Bool]

    init(settings: [Setting : Bool]) {
        self.settings = settings
    }

    func enable(_ setting: Setting) {
        settings[setting] = true
        Networking.request(.updateSetting(setting, isOn: true))
    }

    func disable(_ setting: Setting) {
        settings[setting] = false
        Networking.request(.updateSetting(setting, isOn: false))
    }
}

The above is an example of a very commonly faced, tricky situation - especially when retrofitting old, untested code with unit tests. The problem is twofold - we both need to find some way to substitute the actual networking in our tests, and we're also dealing with a static API which can often be really difficult to mock (since we can't simply inject a mocked instance).

This is a case where taking advantage of Swift's first class function capabilities can provide a really neat solution - again without the need for any additional protocols or mocked instances. Using functional dependency injection, we can create a very simple abstraction that'll hide all networking code from our SettingsManager behind a Syncing function, that we inject in the initializer and then call from our enable and disable methods - like this:

class SettingsManager {
    // We use typealiases to avoid having to repeat the same
    // signatures in multiple places:
    typealias Settings = [Setting : Bool]
    typealias Syncing = (Setting, Bool) -> Void

    private var settings: Settings
    private let syncing: Syncing

    init(settings: [Setting : Bool],
         syncing: @escaping Syncing) {
        self.settings = settings
        self.syncing = syncing
    }

    func enable(_ setting: Setting) {
        settings[setting] = true
        syncing(setting, true)
    }

    func disable(_ setting: Setting) {
        settings[setting] = false
        syncing(setting, false)
    }
}

The beauty of the above approach is that we've now made SettingsManager completely unaware of any networking - it simply calls syncing with the setting it wishes to sync and some other piece of code takes care of the rest, which is perfect in terms of separation of concerns.

Not only does using functions this way give us a very simple syntax internally, it also makes creating an instance of SettingsManager trivial as well. Rather than having to deal with multiple instances of various objects, we simply define a closure that in turn calls our networking API (which can remain static if we wish), which we then inject into our manager:

func makeSettingsManager() -> SettingsManager {
    // We first capture our 'updateSetting' method as a closure
    // in order to be able to pass $0 (the setting) and $1 (the
    // bool flag) directly into it.
    let makeEndpoint = Endpoint.updateSetting
    let syncing = { Networking.request(makeEndpoint($0, $1)) }

    return SettingsManager(
        settings: user.settings,
        syncing: syncing
    )
}

Since our SettingsManager no longer depends on any networking code directly, we can now easily write tests for it. All we have to do is to pass it a syncing closure that simply updates a local settings dictionary, which we can then use for verification:

class SettingsManagerTests: XCTestCase {
    func testSyncingAfterEnablingSetting() {
        var settings = [Setting : Bool]()
        let syncing = { settings[$0] = $1 }

        let manager = SettingsManager(
            settings: [:],
            syncing: syncing
        )

        manager.enable(.profileIsPublic)
        XCTAssertEqual(settings, [.profileIsPublic : true])
    }
}

By using a function for dependency injection this way we've not only made it possible (even easy) to test our SettingsManager, but we've also simplified its internal implementation as well - big win! 🎉

Conclusion

Mocking continues to be an important part of writing tests in Swift, but it's not a silver bullet. Sometimes using alternative techniques - like using functions instead of protocols and creating real instances of our objects - can lead to simpler code that's easier to work with. Like always, having more tools in our "virtual tool-belt" lets us pick the most appropriate technique for any given situation, whether or not that involves creating protocols and mock objects.

We'll continue exploring some of the techniques from this article - especially using functions to manage dependencies - in upcoming articles. The best way to stay up to date with all things Swift by Sundell is to subscribe to the monthly newsletter - which'll give you a recap of all the new content on this site on the 1st day of every month.

What do you think? Do you currently rely heavily on mocks, and will you be able to replace some of them using one of the techniques from this article - or do you have some other preferred way to write mock-free unit tests? Let me know - along with your questions, comments or feedback - on Twitter @johnsundell.

Thanks for reading! 🚀