Automate boilerplate code-generation with Sourcery 🪄
A detailed guide on how we use sourcery - an amazing code generation tool - at Blueground.
Our time is precious! What if someone could write boilerplate Swift code for you? In this article, I will demonstrate a couple of ways we use Sourcery at Blueground along with a short from zero to hero tutorial :)
Before continuing, a big thanks to Krzysztof Zabłocki, the 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 near future we will publish a blog post about template creation (aka stencil cheatsheet) 🥷.
Basic terminology
Before delving into the tutorial, let's get familiar with some basic Sourcery terms.
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:
- Add the template files, responsible for knowing how to convert the generated file. (You will find them in the demo app here 🚀)
- 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.
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:
- 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. - 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
Final thoughts
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 💙.