Swift: testing protocol extension default implementation

Swift is a language that encourages the use of protocols: thanks to protocol extensions and protocol extension default implementation is possible to easily extend complex architectures, but how to test them?

Test Driven Development

In my team we write code applying the TDD process and (almost) every line of code is covered by unit tests: we always try to have a testing mindset, using dependency injection and inversion of control.

Recently I had some problems testing a protocol extensions with a default implementation that calls a class method: injecting a dependency seems to not be an option, so how to create a proper unit test?

Protocol Extensions

A protocol can be extended to provide a default implementation for a property or method: the behaviour is then applied on all objects conforming to the protocol.

Quite powerful, isn’t it?

Project structure

Let’s start creating a simple Evaluable.swift file:

protocol Evaluable {
    var value: Int { get }
}

extension Evaluable {
    var value: Int {
        return 1
    }
}

We have created a protocol and a protocol extension with a default implementation for it: every object that conforms to Evaluable protocol will have a default implementation for the value property.

Let’s now create a simple Car.swift file with a class that conforms to the protocol:

class Car: Evaluable {
    let brand: String

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

The value for our Ferrari will be 1. Not enough for a Ferrari, right?

Class method

Let’s add some code to get the correct value for our shining Ferrari. To keep the code simple, create the Storage.swift file with a class that has a static property where different brands have a different value:

class Storage {
    private static let values = [
        "Audi" : 35,
        "BMW" : 40,
        "Ferrari" : 100
    ]

    public class func getValue(brand: String) -> Int {
        return values[brand] ?? 0
    }
}

If we want to get the value of our car, the protocol default implementation needs to be changed to get data from the Storage:

extension Evaluable where Self: Car {
    var value: Int {
        return Storage.getValue(brand: brand)
    }
}

Unit Tests

Unit tests for the Car class are very simple, especially using Quick and Nimble. Let’s create our CarSpec.swift file and write some test for our car as a safety net for future changes:

class CarSpec: QuickSpec {

    override func spec() {
        var car: Car!

        beforeEach {
            car = Car(brand: "Ferrari")
        }

        it("creates the object") {
            expect(car).toNot(beNil())
        }

        it("has a value of 100") {
            expect(car.value).to(equal(100))
        }
    }
}

There is a problem with this approach: unit tests should test your class only, putting a boundary around it, and everything else should be stubbed. Usually it is done with dependency injection, but the default implementation doesn’t allows us to inject anything and the value variable of the protocol uses the real Storage: time to fix it!

Refactoring

The dependency injection is still the way to follow, but instead of the instance we will inject the type of the storage.

We will dive into each file to understand the changes applied.

Evaluable protocol

First step is to fix the protocol itself: we need to inject our storage, so we won’t use the Storage anymore but we will use the injected storage variable.

extension Evaluable where Self: Car {
    var value: Int {
        return storage.getValue(brand: brand)
    }
}

It doesn’t make sense for the moment because we don’t have the storage variable yet: since the storage will be used by the value, we can add it to the Evaluable protocol as an optional.

Let’s see the final result:

protocol Evaluable {
    var storage: StorageType.Type? { get }
    var value: Int { get }
}

extension Evaluable where Self: Car {
    var storage: StorageType.Type? {
        return nil
    }

    var value: Int {
        let myStorage = storage ?? Storage.self
        return myStorage.getValue(brand: brand)
    }
}

Now if the storage is injected to the car we will use the injected value, otherwise we will use the default implementation.

Car class

We can now change the initialization method of the Car class to accept an injected storage. Making it optional, we can create a Car using only the brand, so we don’t have to change the production code:

class Car: Evaluable {
    var brand: String
    var storage: StorageType.Type?

    init(brand: String, storage: StorageType.Type?) {
        self.brand = brand
        self.storage = storage
    }
}

Storage class

Not many changes: since we need to stub it, let’s create a protocol that will be the base for our dependency injection.

protocol StorageType {
    static func getValue(brand: String) -> Int
}

class Storage: StorageType {
    private static let values = [
        "Audi" : 35,
        "BMW" : 40,
        "Ferrari" : 100
    ]

    public static func getValue(brand: String) -> Int {
        return values[brand] ?? 0
    }
}

Unit Test

Finally we can inject the storage, which allows us to test with dependency injection:

class CarSpec: QuickSpec {

    override func spec() {
        var storage: StorageStub.Type!
        var car: Car!

        beforeEach {
            storage = StorageStub.self
            car = Car(brand: "Ferrari", storage: storage)
        }

        it("creates the object") {
            expect(car).toNot(beNil())
        }

        it("has the correct value") {
            storage.value = 55
            expect(car.value).to(equal(55))
        }
    }
}

class StorageStub: StorageType {
    static var value = 0

    static func getValue(brand: String) -> Int {
        return value
    }
}

Finally everything should work well!

Conclusion

Testing a static method is quite difficult. In general I don’t like to use default implementation and I agree with Natasha idea, but sometimes you can’t skip it.

If you use other techniques to test your protocol extension default implementation, fell free to share 🙂 .

A Xcode project is available on GitHub.

Happy default protocol implementation tests,
M.