Memoji of Jacob giving a thumbs up

Auto-Magically generate mocks using Mockingbird


I personally use the Mockingbird framework to generate my mocks for me. I don't like having to mock an entire interface for every code path I want to test. Mockingbird affords me the convenience and versatility I've been looking for in the Swift world. It does have its limitations, but it's far better than doing all of that work by hand.

If you're not sure what mocking or stubbing is, check out this article.



Installing Mockingbird


To get started, follow the instructions to install and use Mockingbird in your project. They support CocoaPods, Carthage, and Swift Package Manager (SPM). Please follow the instructions to the letter. This isn't a "normal" package. It is generating source code for your test suite on build. So you need to pay attention to the details.

I've done both Carthage and SPM in the past. I prefer the SPM route.



Using Mockingbird


After you've installed Mockingbird in your project's test target, you're ready to rock! 🎸🎸 Before we begin, there are several functions you should get to know in order to effectively use Mockingbird. There is a lot that Mockingbird is capable of, but for this tutorial, we'll focus on the following:



Creating a mock


Mockingbird has an awesome helper function, mock(), that handles most of this for us. It's used rather simply:

let myMock = mock(MyClass.self)

We want to assign the mock to a variable, in most cases, so that we can verify if a function it contains was, or will be, called.



Stubbing the mock


Stubbing is handled with Mockinbird's given function. given will take a mock and one of its functions or variables and allow you to define the result of its invocation. This is handled with the ~> operator like so:

given(myMock.someFunction()) ~> "You da man!"
// or to throw 🤯
given(myMock.someFunction()) ~> { throw SomeError() }



Verifying the Mock


One of the most useful things about mocking, is verifying that a code path was executed. Mockingbird has a verify method that allows us to check if a function was called or not, or more specifically, exactly how many times it was called. It's pretty straight forward:

verify(mock.someFunc()).wasCalled()
// Or if the function should get triggered multiple times
verify(mock.someFunc()).wasCalled(exactly(10))


Mocking Parameters and Return Values


Another incredibly useful feature of Mockingbird is the any() function. This little gizmo will inspect the type of the object it is supposed to fill, and create a mock of that type on the fly. It's incredibly handy if we need to test what happens if a dependency returns nil or not but we don't care about a specific value.

I use this a lot, especially for the harder to define values like the Data type:

given(store.getDataForUser(user: any())) ~> any() // Inline stubbing!!


Doing it, for real....


Let's recreate the example from my last article, but for the tests we'll do things a little differently. Below is a controller that has a logger and a data store. There is a function that reads from the store and logs the event.

protocol Loggable {
    func log(_ message: String)
}

protocol DataStore {
    func getDataFor(_ user: User) -> Data?
}

class Controller {
    let logger: Loggable
    let store: DataStore
    let user: User
    var data: Data? = nil

    init(_ logger: Loggable, _ store: DataStore, _ user: User) {
        self.logger = logger
        self.store = store
        self.user = user
    }

    func loadDataForUser() {
        data = store.getDataFor(user)
        logger.log("data retrieved")
    }
}

What we'll do instead of hand writing the mocks is use the generated mocks provided to us by Mockingbird. The setup will look like this:


class ControllerTests: XCTestCase {
    func testLoadDataForUser() {
        let logger = mock(Logger.self)
        let store = mock(DataStore.self)
        let user = User(named: "Test")
        let controller = Controller(logger, store, user)
    }
}

Then we can Stub the Mock DataStore like:


given(store.getDataForUser(user: user)) ~> any()

And finally we can invoke the controller function and run our assertions:


controller.loadDataForUser()

verify(logger.log(any())).wasCalled()
XCTAssertNotNil(controller.data)


Instead of needing to write a mock for each use case we want to test, we can instead generate them on the fly, and stub their responses only when necessary. This reduces so much overhead when it comes to writing tests that it's tough to describe. I can't tell you how many lines of aggravating configuration code this tool has saved me this year alone.

Tags: