Memoji of Jacob giving a thumbs up

What's Mocking and Stubbing?

And how do those concepts help write cleaner, more effective unit tests in Swift?


Early on, I encountered the concepts of Stubbing and Mocking for the first time. I reacted to the discovery much like I did to testing in general. "What's the point?" I clearly didn't get it. In this article, we'll cover what mocks and stubs are, why they're used, and how they're used effectively.


What's the point?


Are you Mocking me?

Mocking and Stubbing are used primarily for Unit Testing. But what are Mocks, or Stubs? And what is a "Unit" test? Yea... There's a lot of assumed knowledge here. Let's start by laying out some definitions and context:




Unit Testing


Unit testing is the act of writing tests where we run a small chunk of code in isolation, specifically, a single function. This "unit" should do exactly A under X conditions or exactly B under Y conditions etc., We control the variables and observe the outcomes at a very small scale.

Because unit tests target singular functions in the code base, we are able to verify how all the little pieces work in isolation. This gives us confidence that multiple units will integrate together without any surprises. And, what I like most about unit tests, when a test is written well, automated checks catch bugs before they ship to production.

A more specific description can be found on stack overflow, among other places. Pure Functions are ideal for unit testing.




Mocks


Mocks are test doubles of an object that we place inside of our tests. They operate exactly as they are told to, and report back to you what happened in the test.

Mocks, when created, have the same interface as its target, but none of the functionality. Instead, the mock can be told to return stubbed data or to verify whether or not a mock's function was executed.




Stubs


Stubs are hardcoded responses that we can force into our test code that allow us to easily test a function fully, without having to configure the rest of the environment to achieve all of the possible outcomes.

Here is a good summary of mocking and stubbing if you'd like more info.




But Why?


There are 3 primary reasons I use mocking and stubbing. First, there is a significant reduction in the amount of boilerplate code required to configure each test. Second, we can prevent noisy or expensive tasks from occurring, like log outs or database reads and writes, by replacing those dependencies with mocks. Finally, we can automate tests that verify a code path was executed this can not easily be done without the mock. In short, Mocking and Stubbing allow us to focus on the code we're writing.



Mocking and Stubbing allow us to focus on the code we're writing.



Writing Mocks and Stubs in Swift

Swift is an awesome language. There's a lot to love about it, but, its implementation of reflection is somewhat limited. Because of that, there used to be no good way to generate a mock and so, many developers have been manually creating them. Fortunately, Swift if is a Protocol Oriented Programming Language, and we can account for this slight shortcoming.



Hand Rolled Mocks


Much of the Swift community manually creates the mocks they need as they need them. To demonstrate how this is done, we'll make a simple example of a controller, a data store, and a logger using Protocols. Note the following example does not actually create a logger or store, only their interface.

This is what the implementation might look like:



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")
    }
}


The function loadDataForUser should read from a data store, log that it did so, and update the controller's data value. To test this code without actually logging an event or reading from our databases, we will need a mocked version of a logger and a data store. That might look something like this:



struct MockLogger: Loggable {
    var wasCalled = false

    func log(_ message: String) {
        wasCalled = true
    }
}

struct MockStore: DataStore {
    func getDataFor(_ user: User) -> Data? {
        "".data(using: .utf8)
    }
}


These mocks will allow us to verify that Controller.loadDataForUser modifies the data variable, and that the logger gets called (which is near impossible to do without a mock). We do this like so:



class ControllerTests: XCTestCase {
    func testLoadDataForUser() {
        // Given
        let logger = MockLogger()
        let controller = Controller(logger, MockStore(), User(named: "Test"))

        XCTAssertFalse(logger.wasCalled)
        XCTAssertNil(controller.data)
        
        // When
        controller.loadDataForUser()
        
        // Then
        XCTAssertTrue(logger.wasCalled)
        XCTAssertNotNil(controller.data)
    }
}


In that example we mocked two interfaces and effectively tested the Controller. Because of the mocks, we were able to verify the functionality of the Controller without testing the functionality of the DataStore or the Logger. The actual implementations of those will get tested in isolation.

Now, you may have noticed that there was a bit of configuration code required to run those tests. And I told you that mocking reduces the amount of test configuration we need to write.

They do. Even hand rolled mocks like these do. But I prefer to have my mocks generated for me in such a way that eliminates the need to create one off mocks like this. Luckily, there is a package that will do just that. In my next article, you'll learn how to generate fully functional mocks using mockingbird 😊

-> Auto-Magically generate mocks using Mockingbird ->



Tags: