Protocol Oriented Programming + Unit Testing = ♥️

In 2015, Apple introduced a new concept - Protocol Oriented Programming. I encourage you to watch the whole 50 minutes of the WWDC video. One of the conference's speekers laid down all new awesome Swift features and how protocols can help write a better, robust code.

I'm sure, you are using most of these tricks, maybe all. Isn't it great? Of course it is, but Swift introduced some restrictions. For instance, you can't swizzle Swift method. You can do it with @objc methods. Test frameworks, like OCMock, use this feature to dynamically change method implementation (in simple words, of course). But what about Swift classes and their methods? Well... You can "generate" mocks as a build step in Xcode, I don't think this is a perfect solution.

The problem

OK. You are writing some feature utilising UserDefaults. Maybe a simple saving user decision regarding dark theme preference.

class PreferenceSaver {
    var useDarkTheme: Bool {
        get {
            UserDefaults.standard.bool(forKey: "useDarkTheme")
        }
        set {
            UserDefaults.standard.set(newValue, forKey: "useDarkTheme")
        }
    }
}

So far, this would work perfectly in production, but... What if requirements change? Maybe standard user defaults are not good enough? For one property, change is not problematic, but if there is 20 preferences... Man, that is a huge pull request. I'll refactor this to make it more usable:)

class PreferenceSaver {
    private let defaults: UserDefaults
    
    init(defaults: UserDefaults = .standard) {
        self.defaults = defaults
    }
    
    var useDarkTheme: Bool {
        get {
            defaults.bool(forKey: "useDarkTheme")
        }
        set {
            defaults.set(newValue, forKey: "useDarkTheme")
        }
    }
}

Not much has changed, but everything's changed. Now PreferenceSaver has a new dependency, injectable in init. Furthermore, I added a default value - so no changes in codebase to accomodate new initialisation.

We are half way done. Now unit tests. To inject UserDefaults mock, you have to inherit and impelent a whole Apple class! It's overwhelming and cumbersome. Defaults class is quite simple, what about location services, Store Kit classes? They are huge and complicated.

I'll refactor the code once more.

protocol UserDefaultsProtocol {
    func bool(forKey: String) -> Bool
    func set(_ value: Bool, forKey: String)
}

extension UserDefaults: UserDefaultsProtocol { }

class PreferenceSaver {
    private let defaults: UserDefaultsProtocol
    
    init(defaults: UserDefaultsProtocol = UserDefaults.standard) {
        self.defaults = defaults
    }
    
    var useDarkTheme: Bool {
        get {
            defaults.bool(forKey: "useDarkTheme")
        }
        set {
            defaults.set(newValue, forKey: "useDarkTheme")
        }
    }
}

Once again, my change doesn't affect current codebase at all, so no changes in files using PreferenceSaver. By adding protocol and extending UserDefaults, we have all functionalities of the original implementation with a HUGE addition, testability!

In a test suite, inject mock to PreferenceSaver:

class PreferenceSaverMock: UserDefaultsProtocol {
    var dictionary = [String: Any]()
    func bool(forKey: String) -> Bool {
        return dictionary[forKey] as! Bool
    }
    
    func set(_ value: Bool, forKey: String) {
        dictionary[forKey] = value
    }
}

Now assert everything, if the key is present, if the value is proper. We are completly untangled from Apple and their implementation. When using UserDefaultsProtocol, no requirements changes are scary. Core Data? No problem, saving to plist? Easy peasy, and at the end, code can be covered in 100% without any hassle. Happy coding and testing!


Extra credit

To make everything perfect, I'll remove "magic strings" from code :)

let darkThemeKey = "useDarkTheme"
var useDarkTheme: Bool {
    get {
        defaults.bool(forKey: darkThemeKey)
    }
    set {
        defaults.set(newValue, forKey: darkThemeKey)
    }
}

Happy coding!