Automate Boilerplate Code-Generation With Sourcery ๐Ÿช„

A detailed guide on how we use sourcery, a tool for autogenerating code, in Blueground.

Konstantinos Nikoloutsos

12 minute read

Our time is precious! What if someone could write boilerplate Swift code for you? In this article, I will show you how we use Sourcery in Blueground.

Before continuing a big thanks to Krzysztof Zabล‚ocki, creator of Sourcery โค๏ธ

๐Ÿ˜ฑ Sourcery has saved us from writing and maintaining 4000+ lines of code

โœ… Use-cases :

- AutoMockable ๐Ÿ’ก

We all know how important it is to unit test your code as it allows you to refactor it later without introducing new bugs.

AutoMockable will create Mocks for your tests. All you have to do is to comment your protocol with // sourcery: AutoMockable

// sourcery: AutoMockable
protocol InboxDetailDisplayLogic: AnyObject {
    func displayInitView(viewModel: InboxDetail.InitView.ViewModel)
    func displayShowDetail(viewModel: InboxDetail. ShowDetail. ViewModel)
    func displayOpenUrl(viewModel: InboxDetail. OpenURL. ViewModel)
}

And tada ๐Ÿช„

// Generated using Sourcery 1.8.1 โ€” https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// ๐Ÿ’™๐Ÿ’™ Autogenerated for saving Beegees time โฐ
// swiftlint:disable all
@testable import Blueground

final class InboxDetailDisplayLogicSpy: InboxDetailDisplayLogic {
    // MARK: - displayInitView
    var closureDisplayInitView: () -> () = {}
    var invokedDisplayInitView = false
    var invokedDisplayInitViewCount = 0
    var invokedDisplayInitViewParameters: (viewModel: InboxDetail.InitView.ViewModel, Void)?
    var invokedDisplayInitViewParametersList = [(viewModel: InboxDetail.InitView.ViewModel, Void)]()

    func displayInitView(viewModel: InboxDetail.InitView.ViewModel) {
        invokedDisplayInitView = true
        invokedDisplayInitViewCount += 1
        invokedDisplayInitViewParameters = (viewModel, ())
        invokedDisplayInitViewParametersList.append((viewModel, ()))
        closureDisplayInitView()
    }
    // MARK: - displayShowDetail
    var closureDisplayShowDetail: () -> () = {}
    var invokedDisplayShowDetail = false
    var invokedDisplayShowDetailCount = 0
    var invokedDisplayShowDetailParameters: (viewModel: InboxDetail.ShowDetail.ViewModel, Void)?
    var invokedDisplayShowDetailParametersList = [(viewModel: InboxDetail.ShowDetail.ViewModel, Void)]()

    func displayShowDetail(viewModel: InboxDetail.ShowDetail.ViewModel) {
        invokedDisplayShowDetail = true
        invokedDisplayShowDetailCount += 1
        invokedDisplayShowDetailParameters = (viewModel, ())
        invokedDisplayShowDetailParametersList.append((viewModel, ()))
        closureDisplayShowDetail()
    }
    // MARK: - displayOpenUrl
    var closureDisplayOpenUrl: () -> () = {}
    var invokedDisplayOpenUrl = false
    var invokedDisplayOpenUrlCount = 0
    var invokedDisplayOpenUrlParameters: (viewModel: InboxDetail.OpenURL.ViewModel, Void)?
    var invokedDisplayOpenUrlParametersList = [(viewModel: InboxDetail.OpenURL.ViewModel, Void)]()

    func displayOpenUrl(viewModel: InboxDetail.OpenURL.ViewModel) {
        invokedDisplayOpenUrl = true
        invokedDisplayOpenUrlCount += 1
        invokedDisplayOpenUrlParameters = (viewModel, ())
        invokedDisplayOpenUrlParametersList.append((viewModel, ()))
        closureDisplayOpenUrl()
    }
}

So now you automatically created the mocks you want ๐Ÿ”ฅ. How awesome is that?!

Files created by Sourcery have a suffixย .generated

Oh, and 1 more thing, Automockable supports compositional protocols โญ๏ธ.

// sourcery: AutoMockable
private typealias LookWhatICanDo = Foo & Calculator
// Generated using Sourcery 1.8.1 โ€” https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// ๐Ÿ’™๐Ÿ’™ Autogenerated for saving Beegees time โฐ
// swiftlint:disable all

@testable import FooApp
import Foundation

final class LookWhatICanDoSpy: Foo, Calculator {
    // MARK: - โšก๏ธ Foo
    // MARK: - foo
    var closureFoo: () -> () = {}
    var invokedFoo = false
    var invokedFooCount = 0
    var stubbedFooResult: Double!

    func foo() -> Double {
        invokedFoo = true
        invokedFooCount += 1
        closureFoo()
        return stubbedFooResult
    }
    // MARK: - โšก๏ธ Calculator
    // MARK: - add
    var closureAdd: () -> () = {}
    var invokedAdd = false
    var invokedAddCount = 0
    var invokedAddParameters: (n1: Decimal, n2: Decimal)?
    var invokedAddParametersList = [(n1: Decimal, n2: Decimal)]()
    var stubbedAddResult: Decimal!

    func add(n1: Decimal, n2: Decimal) -> Decimal {
        invokedAdd = true
        invokedAddCount += 1
        invokedAddParameters = (n1, n2)
        invokedAddParametersList.append((n1, n2))
        closureAdd()
        return stubbedAddResult
    }
    ...

โœ… You don’t have to worry about a missing import statement in your spies because it is handled automatically.

โœ… Supports async functions

โœ… Supports compositional protocols

- AutoFixturable ๐Ÿ’ก

This is handy for those using the Fixture Object Pattern. Fixtures, a well-known pattern, makes creating new instances of an object easier.

๐Ÿ—ฃ You can think of fixtures as data

If you haven’t heard of it before, here is an example that will reveal its power ๐Ÿ‘‡

struct Human {
    let firstName: String
    let lastName: String
    let fatherName: String
    let motherName: String
    let address: Location
    let age: Int
}

Now assume you write a test where you want your service to work only for adults (age โ‰ฅ 18). Do you really care about properties different than age?

Without fixtures you would manually create the object:

func test_HumanAgeEligible {
  // Given
  let human = Human(
    firstName: "John",
    lastName: "Jow",
    fatherName: "Nick",
    motherName: "Maria",
    address: .init(),
    age: 14
  )
  
  // When
  let result = service.hasEligibleAge()
  
  XCTAssertFalse(result)
}

And with fixtures we have this:

func test_HumanAgeEligible {
  // Given
  let human = Human.fixtures(age: 14) // My girlfriend is happier as I write less (mechanical keyboard ๐Ÿ˜†)
 
  // When
  let result = service.hasEligibleAge()
  
  XCTAssertFalse(result)
}

Think how complex it is to create an object that has nested structs. Fixture pattern is an elegant way to simplify your tests and focus on properties you care about.

But wait for a second, who implements this static function named fixtures()? Not you, Sourcery will! All you have to do is annotate your struct.

// sourcery: AutoFixturable
struct Human {
    // sourcery: example = ""The fixture will have this string as default value""
    let firstName: String
    let lastName: String
    let fatherName: String
    let motherName: String
    // sourcery: example = ".init()"
    let address: Location
    let age: Int
}

And tada ๐Ÿช„

// Generated using Sourcery 1.8.1 โ€” https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// ๐Ÿ’™๐Ÿ’™ Autogenerated for saving Beegees time โฐ
// swiftlint:disable all
@testable import 
import Foundation

extension Human {
    static func fixtures(
        firstName: String = "The fixture will have this string as default value",
        lastName: String = "Lorem ipsum",
        fatherName: String = "Lorem ipsum",
        motherName: String = "Lorem ipsum",
        address: Location = .init(),
        age: Int = 666
    ) -> Human {
        return .init(
            firstName: firstName,
            lastName: lastName,
            fatherName: fatherName,
            motherName: motherName,
            address: address,
            age: age
        )
    }
}

As you can see, by using example annotation, you can instruct Sourcery on what value to use as the default param in the fixtures(). More on this later on how it works!

If you are interested in how to create your own template, stay tuned as in the early future we will publish a blog post about template creation (aka stencil cheatsheet) ๐Ÿฅท.

Basic terminology

Here are some terms in case you haven’t used Sourcery before:

Stencil:

A template language that you use to create templates. Templates will be used by Sourcery to autogenerate code.

Template:

Is the instructions you give to Sourcery to autogenerate code. AutoMockable and AutoFixturable are templates. Templates are usually written in Stencil.

Sourcery.yml:

The YAML file is used for configuring Sourcery. You basically define:

  • input: What source files will be used from Sourcery.
  • templates: What templates you want to run on those files
  • output: Where and how to save the generated files
  • args: Used for passing parameters to templates e.g. testableImport is passed as a parameter to AutoMockable template and is the module name that will be testableImported.

Tutorial time ๐Ÿ’™:

โญ๏ธ Final demo click here

Every tutorial begins with a FooApp project ๐Ÿ˜†. So letโ€™s create one.

1. Create a fooApp (optional)

2. Install Sourcery

Now that we have created our app, we have to install Sourcery on our machine. We will do that with HomeBrew (keep in mind there many ways to download Sourcery).

This will download the Sourcery binary and save it in /opt/homebrew/bin/sourcery so that we can access it right away from our terminal. Just to make sure everything is okay, run the following:

3. Annotate a protocol with AutoMockable in your project

Letโ€™s create the following protocol.

Now letโ€™s try to create the generated files. From the root project path run:

Don’t panic, we haven’t provided Sourcery with the necessary data that needs to work. We still have to do the following:

  1. Add the template files, responsible for knowing how to convert the generated file. (You will find them in the demo app here ๐Ÿš€)
  2. Make a sourcery.yml which we will use to instruct Sourcery:
  • What input files to parse
  • What templates to use
  • Where to place the generated code

With all that, Sourcery will have all the information it needs to auto-generate files.

๐Ÿ’ก .yml stand for yet another markdown, and most of the time, it is used for configuration. Just like SwiftLint has a configuration yml.

4. Integrate Sourcery into the project

This is the last step ๐ŸŽ‰

Letโ€™s add the Automockable.stencil template in a folder called Sourcery.

๐ŸšจOnce again, you can find the contents of AutoMockable.Stencil in FooApp repo click here.

And now that we have our template, we are ready to create the .sourcery.yml.

The file with prefix . is a hidden file and in order to hide/show you have to press command+ shift + .

Open the terminal and write:

touch .sourcery.yml 

then open it with your favorite editor and put the following for its content.

# READ MORE ABOUT configuration here https://merowing.info/Sourcery/usage.html
configurations:
    # -- Blueground configuration --
    - sources:
        include:
          - FooApp
      templates:
        - Sourcery
      output:
          path: FooAppTests/Sourcery
          link:
            project: ./FooApp.xcodeproj
            target: FooAppTests
            group: FooAppTests/Sourcery
      args:
        testableImport: "FooApp" # [MANDATORY] Your mocks will have "@testable import <testableImport>"
        containedPathSubstringToApplyTemplate: "/FooApp/" # [MANDATORY] If a protocol with Automockable annotation exists but it's path doesn't contain <focusFolder> it will be ignored.
  • Sources โ†’ The only files Sourcery will read/parse
  • templates โ†’ The instructions Sourcery will use for creating the autogenerated code
  • output โ†’ In that way, we auto-link the generated files into our project (added in .pbxproject)
  • Arg testableImport โ†’ It will add the testable import FooApp on top of every mock/spy
  • Arg containedPathSubstringToApplyTemplate โ†’ This is because if we add more sources located in different places package/pod. And they have a protocol annotated with AutoMockable. We donโ€™t want to autogerate for this in FooApp tests. Hence the name, the file path of that parsed files need to contain the substring /FoodApp/.

5. The moment we all waited for ๐Ÿš€

Letโ€™s see the output ๐Ÿ”ฅ

so now you can generate mocks just by annotating them, opening the terminal, and running sourcery. Isnโ€™t that beautiful?

โ„น๏ธ In Blueground we commit the .generated files into the repository. So it is not required to run Sourcery for building the project

But Autofixturable is not integrated yet.

To do that, we have to add the AutoFixturable.stencil in the Sourcery folder. And also, add a FixturableCommonExamples.swift that is responsible for providing default values on certain types that we haven’t annotated // sourcery example .

Also, change the sourcery.yml to the following, allowing Sourcery to read and parse the FixturableCommonExamples.swift for using it.

# READ MORE ABOUT configuration here https://merowing.info/Sourcery/usage.html
configurations:
    # -- Blueground configuration --
    - sources:
        include:
          - FooApp
          - Sourcery # We need to read the FixturableCommonExamples.swift
      templates:
        - Sourcery
      output:
          path: FooAppTests/Sourcery
          link:
            project: ./FooApp.xcodeproj
            target: FooAppTests
            group: FooAppTests/Sourcery
      args:
        testableImport: "FooApp" # [MANDATORY] Your mocks will have "@testable import <testableImport>"
        containedPathSubstringToApplyTemplate: "/FooApp/" # [MANDATORY] If a protocol with Automockable annotation exists but it's path doesn't contain <focusFolder> it will be ignored.

Great, now you can create fixtures :) Clone the demo app to try it yourself click here

Extra tips ๐Ÿ“”

- How to integrate Sourcery in all your modules

Wait for a second; this will only work for your main module/project. But what if you want to auto-generate the mocks for your SPM modules? You would have to append a new configuration to your .sourcery.yml .

๐Ÿ”ฅ Keep in mind that .sourcery.yml supports multiple-configurations that is equivelant to running each configuration one by one.

So suppose we had one SPM module named Analytics located in ./SwiftPackage/Analytics then we would have to rewrite the configuration as follows.

# READ MORE ABOUT configuration here https://merowing.info/Sourcery/usage.html
configurations:
    # -- Blueground configuration --
    - sources:
        include:
          - FooApp
      templates:
        - Sourcery
      output:
          path: FooAppTests/Sourcery
          link:
            project: ./FooApp.xcodeproj
            target: FooAppTests
            group: FooAppTests/Sourcery
      args:
        testableImport: "FooApp" # [MANDATORY] Your mocks will have "@testable import <testableImport>"
        containedPathSubstringToApplyTemplate: "/FooApp/" # [MANDATORY] If a protocol with Automockable annotation exists but it's path doesn't contain <focusFolder> it will be ignored.
    - sources:
      - SwiftPackages/Analytics # -- In case your SPM depends on other modules it is REQUIRED to add them here --
      templates:
        - Sourcery
      output:
        SwiftPackages/Analytics/Tests/AnalyticsTests/Mocks
      args:
        testableImport: "Analytics"
        containedPathSubstringToApplyTemplate: "/SwiftPackages/Analytics/"

Observe how testableImport and containedPathSubstringToApplyTemplate changed. Now, if the Analytic SPM module contains a protocol annotated with AutoMockable it will autogenerate the mock for us in the path we specified in the output section.

SPM packages do not need to be linked in a pbxproject. So it is okay to save them just in folder. Hence the different output configuration in config

So that way you can keep adding modules that will have the ability to autogenerate mocks with sourcery.

- Generate mock from a protocol that is located in another module (e.g pod/spm etc):

Assuming I am into FooApp and want to write a test that requires a mock on a third-party protocol. No worries, here is how to do it:

  1. Create a file named externalBridge.swift in your project. The name just reminds us that it acts as a bridge for external files outside the current module. This file’s job is to pass some information to Sourcery.
  2. Add the following

Now you will be able to run Sourcery and create mocks for StripePayment protocol which is implemented in an external place where you cannot edit that code.

DisableContainedPathSubstringToApplyTemplate Will turn off the following we have added in the .sourcery.yml. Otherwise, no mock would be created because the path of StripePayment will not contain "FooApp" substring.

extraImport will add a testable import of what you want. In our case of Stripe we will use Stripe name module.

And thatโ€™s it. Now if we would run Sourcery it would generate the mock for us even if the protocol is located outside.

- Autofixturable example annotation:

// sourcery: example annotation not only acts as a message for Sourcery but it enhanhes documetantation. See markdown property above ๐Ÿ‘†.

When using annotation example for autofixturable always add your content inside โ€œโ€. So if you want to write a string you will add double quotes // sourcery: example = "โ€I am a stringโ€โ€ That way, the Autofixturable template will use what is inside the first quotes. Here are some examples of our codebase that use Autofixturable๐Ÿช„

- Testing your custom Sourcery templates

Here in Blueground, we prefer automatic testing done by unit tests rather than manual testing. Thatโ€™s because manual testing takes more time and also contains human error.

So how can we test that Sourcery is generating the expected output? More specifically, how can we test that a change in a template does not create a bug.

The solution: snapshot testing ๐Ÿ’ก

In another separate article, we will thoroughly discuss this in more detail

- The end

Our time is precious! Sourcery can boost our productivity significantly and allow us to focus on things that matter! I hope you liked this tutorial, and if you have any questions or anything to share with us, do not hesitate to leave a comment :)

If you liked what you read, don’t hesitate to share it with your colleagues/friends ๐Ÿ’™.

comments powered by Disqus