JakeFake

A quick and dirty helper for making test fakes.

The problem to solve

When writing unit tests, we want to test specific units of code, decoupled from the actual state of our application. To do this, we simulate the other units of code with which the subject communicates. Doing this is referred to as putting the subject into a “test harness.”

These simulated units of code are what we generally call “fakes.” They allow the test code to change the conditions around the subject to replicate known app states/conditions, such as the success or failure of a network call, or a specific state on a helper class. Changing how these fakes respond to being interacted with by the subject is called “stubbing” and recording whether or methods or properties have on the fake have been accessed is called “spying.”

JakeFake is a bare-bones class and protocol for creating test fakes for stubbing/spying in Swift, inspired by the Spry framework. It has two main components:

The JakeFaker object:

This object encapsulates most of the JakeFake functionality. It works as a helper object to manage the spying and stubbing for a given fake. It has a generic Function type which allows the user to implement any kind of object to define their method captures as long as that object conforms to JakeFakeFunction which simply is a bundling of the Equatable and Hashable protocols.

The JakeFake protocol:

This protocol is what your fake objects will actually conform to. It’s designed to closely mirror the JakeFaker object, so that in tests you can inspect the fake itself rather than having to inspect its .faker property. Since the majority of the protocol is intended to be passthrough to the faker, I have included a protocol extension which does exactly that. And thanks to that, all one needs to do to conform to the JakeFake is to define their Function type, and implement the faker property, which will include that type as its generic.

A bit of advice

When you define your Function type, I recommend using an enum, as it gives you a finite set of cases, each corresponding to a given method, which you can configure any number of ways. Swift enum associated values are also an excellent tool for capturing the arguments of the methods. This gives you a strong typing for the capturing and comparing of method calls. For example, a method with the signature

func doStuff(toString string:String, doOtherStuff:Bool) -> String

would have a corresponding case in the enum

case doStuff(String, Bool)

How to use it:

Imagine, if you will, that you have a class called Dog:

class Dog {

    private var stomach = [DogFood]()
    private var shouldPoop = false

    func bark() -> String {
        return ["woof!", "bork!", "yip!"].randomElement() ?? "glorf?"
    }

    func eat(food: DogFood) {
        stomach.append(food)
    }

    func digest() -> String? {
        if shouldPoop {
            let firstEaten = stomach.removeFirst()
            return firstEaten.digested
        }
        shouldPoop = !shouldPoop
        return nil
    }
}

Conforming to JakeFake

Dog is a dependency in a class, HappyFamily, that you’re testing. As such, you want to generate a fake so that you can control Dog’s behavior in your tests. JakeFake allows you to create a fake simply by creating a subclass of Dog that conforms to the JakeFake protocol, and overriding its methods. Check it out!

class FakeDog: Dog, JakeFake {
    enum Function: JakeFakeFunction {
        case bark
        case eat(DogFood)
        case digest

        public static func ==(lhs: Function, rhs: Function) -> Bool {
            switch (lhs, rhs) {
            case (.eat(let food1), .eat(let food2)):
                return food1 == food2
            case (.bark, .bark), (.digest, .digest):
                return true
            default:
                return false
            }
        }

        public var hashValue: Int {
            switch self {
            case .bark:
                return 0
            case .eat(_):
                return 1
            case .digest:
                return 2
            }
        }
    }

    let faker: JakeFaker<Function> = JakeFaker()

    //MARK: - overrides

    override func bark() -> String {
        recordCall(.bark)
        return stubbedValue(method: .bark, asType: String.self)!
        //stubbedValue(method:asType:) returns an optional, so it needs to be unwrapped
    }

    override func eat(food: DogFood) {
        recordCall(.eat(food))
    }

    override func digest() -> String? {
        return recordAndStub(method: .digest, asType: String.self)
        // This is a convenience method which calls both recordCall and stubbedValue
    }
}

Spying on our fake

Wasn’t that easy?

Now that we have our FakeDog object, if we want to verify that a method has been called on Dog, for example: eat(food:), we can simply call received(method:):

let dog = FakeDog()
dog.eat(food: .tableScraps)

dog.received(method: .eat(.tableScraps))  //evaluates to true
Ignoring arguments.

And with our Function type defined as we do above, we can differentiate between a method being called with specific arguments and a method being called period. To do this we set the ignoreArguments parameter to true (it has a default value of false).

let dog = FakeDog()
dog.eat(food: .dry)
dog.eat(food: .wet)

dog.received(method: .eat(.tableScraps))                            //evaluates to false
dog.callCountFor(method: .eat(.tableScraps))                        //evaluates to 0

dog.received(method: .eat(.tableScraps), ignoreArguments: true)     //evaluates to true
dog.callCountFor(method: .eat(.tableScraps), ignoreArguments: true) //evaluates to 2

For the time being, you still have to provide the associated values, but they’ll be ignored, as seen above.

Stubbing methods on our fake

For methods on Dog that have return types, such as digest we can also use JakeFake to stub out what that method will return. So, in our setup, we can do the following:

dog.stub(.digest) { () -> Any? in
    return "Upset tummy"
}

And as the following code will evaluate as shown:

dog.digest()  //evaluates to "Upset tummy", the stubbed value above
dog.received(method: .digest)  //evaluates to true

Thanks!

And that’s about it! Feel free to ask me all about it, and report any bugs you find.

To be clear, I made this in an afternoon as a stopgap measure, and it works, but is mainly just a fun project. If you want a more stable, strongly-typed, fully-featured stubbing/spying framework, I recommend using Spry.


© 2021 Jake Hawken