Using Sourcery to Automatically Generate Mocks

At my current workplace, our app uses the MVVM architecture. One of the reasons we made the switch in the first place was so we could unit test business logic, so we try to test all of our View Models. We only use XCTest — which I actually like — but there are a few shortcomings that made writing tests feel like a chore instead of part of the development process.

One of the parts I particularly dislike is creating mocks for our dependencies. Almost every View Model interacts with our backend/local storage in some way, and they do so with the help of Repos. I'm not gonna dig deep into their implementation since lots of projects use the same kind of abstraction, but they're a way to fetch the entities we're concerned with. We use RxSwift to provide these entities as Observables, but that's about it. Here's a sample implementation of a Repo.

class ItemRepo {
    private let service: ItemService
    init(service: ItemService = ItemService()) {
self.service = service
}
    func getItems() -> Observable<[Item]> {
return self.service.getItems()
}
}

We would then inject an ItemRepo in an imaginary ItemListViewModel and the Repo would be tasked with fetching our items.

Of course, when we're testing our app we don't actually want to make a request to our backend — we need our tests to be reliable and have no external dependencies. To do this, we'd inject a MockItemRepo, which would provide our previously defined Items to the View Model.

There are lots of ways to implement Mocks, but we use a relatively simple way so that we can easily change the response for any specific Repo method. This ended up being important to us as we test pagination, for example, since the Repos themselves cannot be swapped from a View Model after initialization. Below is a possible implementation of a Mock for an ItemRepo.

class MockItemRepo: ItemRepo {
    var response_getItems: [Item]? = nil
var error_getItems: Error? = nil
    override func getItems() -> Observable<[Item]> {
if let error = error_getItems {
return Observable.error(error)
} else if let response = response_getItems {
return Observable.just(response)
} else {
preconditionFailure("Did not provide a mock response for getItems")
}
}
}

As you can see, the implementation itself is very simple, but there are a few problems which make this solution scale very poorly. First, there are 11 lines of boilerplate code. This code has to be replicated for every single method, and you still have to create a new class for every new Repo. It's easy to see how time-consuming this can become.

It's also easy to forget — if we accidentally forget to override a method and it ends up being used in the View Model, we can introduce weird side effects to our tests. No one likes seeing a test fail on your CI pipeline when they all pass on your machine.

To solve these issues, we use Sourcery. Sourcery is a meta-programming tool for Swift — that is, we can use it to generate code for us by writing a template. Boilerplate code like this is a perfect example of why you'd use Sourcery, as it saves time and ensures safety as you change your original sources.

Sourcery has great documentation and even some sample templates, so I'll focus on the process of building my template. We'd start by creating your template and then telling Sourcery where our source code is located, where our templates are and where our generated code should be. We can also tell it to watch for changes in our templates, so that we can see the changes in real time. (That is actually how I like to build my templates — I'll have the template and the generated file both open in Visual Studio Code and every time I save my changes I can instantly see the results.)

mkdir Generated
mkdir SourceryTemplates
touch SourceryTemplates/AutoMockableRepo.stencil
sourcery --sources SourcesPath/ --templates SourceryTemplates/ --output Generated --watch

Having done that, we can start thinking about how we're gonna build our template. My idea is to have a protocol called AutoMockableRepo to use as a marker for Repos we want to generate Mocks automatically for us. After creating this protocol and making ItemRepo conform to it, we can start working on our template.

import Foundation
import RxSwift
@testable import YourModule
{% for type in types.implementing.AutoMockableRepo %}
// MARK: {{type.name}}: AutoMockableRepo
{% endfor %}

If we save this as our template, we should see a comment generated for our ItemRepo type. You can read a little more about Stencil templates to follow along, but we're gonna follow this idea until we end up with our desired result.

{% macro swiftifyMethodName name %}{{ name | replace:"(","_" | replace:")","" | replace:":","_" | replace:"`","" | snakeToCamelCase | lowerFirstWord }}{% endmacro %}
{% macro getReturnTypeFromObservableName name%}{{ name | replace:"Observable<","" | replace:">","" }}{% endmacro %}
import Foundation
import RxSwift
@testable import YourModule
{% for type in types.implementing.AutoMockableRepo %}
// MARK: {{type.name}}: AutoMockableRepo
class Mock{{type.name}}: {{type.name}} {
{% for method in type.methods %}{% if not method.isInitializer %}
var response_{% call swiftifyMethodName method.selectorName %}: {% call getReturnTypeFromObservableName method.returnTypeName %}? = nil
var error_{% call swiftifyMethodName method.selectorName %}: Error? = nil
    override func {{method.name}} -> {{method.returnTypeName}} {
if let error = var error_{% call swiftifyMethodName method.selectorName %} {
return Observable.error(error)
} else if let response = response_{% call swiftifyMethodName method.selectorName %} {
return Observable.just(response)
} else {
preconditionFailure("Did not provide a mock response for {{method.selectorName}}")
}
}
{% endif %}{% endfor %}
}
{% endfor %}

When you run this template, it should create a class that's identical to the code for the MockItemRepo we implemented by hand. With the use of a borrowed macro from the Sourcery samples and a (poorly written) custom macro to retrieve the response type from the Observable, we can quickly and easily make Sourcery create the boring stuff for us, while focusing on our actual tests.

You'll probably need to adapt this template for your own needs — ours is quite a bit longer, dealing with some more complex initialization needs. Even so, it did not take long to build at all, especially when you consider the development time saved.

Finally, you can either add a new build phase to update the generated file along with every single build or you can run them manually when you need it to be updated. Personally, I've chosen to run it manually to avoid issues during development, but I've created a simple bash script to run it without needing to remember the long command for it and put it in our pipelines.

That's it! If you use Sourcery for other cool stuff in your project, feel free to tell me on Twitter! I'm always interested in making our lives as developers easier. 😉