Taking the boilerplate out of
your tests with Sourcery
Vincent Pradeilles (@v_pradeilles) – Worldline
!
2
3
import XCTest
class MyTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
}
4
5
!
6
Sourcery 101
7
What is Sourcery?
8
What is Sourcery?
"Sourcery is a code generator for the Swift language,
built on top of Apple's own SourceKit. It extends the
language abstractions to allow you to generate
boilerplate code automatically."1
1 
https://github.com/krzysztofzablocki/Sourcery/blob/master/README.md
9
What can we do with Sourcery?
10
What can we do with Sourcery?
enum Direction {
case up
case right
case down
case left
}
Direction.allCases // [.up, .right, .down, .left]
11
What can we do with Sourcery?
Since Swift 4.2, this can be achieved through the
CaseIterable protocol.
But before, generating .allCases was a great use case
to illustrate how Sourcery works.
12
Let's implement it
13
1st – Setting Up Sourcery
14
Setting Up Sourcery
$ brew install sourcery
15
Setting Up Sourcery
Xcode Project > Build Phases > New Run Script Phase
sourcery 
--sources <sources path> 
--templates <templates path> 
--output <output path>
16
2nd – Phantom Protocol
17
Phantom Protocol
protocol EnumIterable { }
extension Direction: EnumIterable { }
18
3rd – Sourcery Template
19
Sourcery Template
{% for enum in types.implementing.EnumIterable|enum %}
{% if not enum.hasAssociatedValues %}
{{ enum.accessLevel }} extension {{ enum.name }} {
static let allCases: [{{ enum.name }}] = [
{% for case in enum.cases %}
.{{ case.name }} {% if not forloop.last %} , {% endif %}
{% endfor %}
]
}
{% endif %}
{% endfor %}
20
4th - Generated Code
21
Generated Code
Build your target
22
Generated Code
internal extension Direction {
static let allCases: [Direction] = [
.up ,
.right ,
.down ,
.left
]
}
23
Generated Code
Add the generated file to your project
That's it
!
24
To Sum Up
25
To Sum Up
→ Sourcery parses your source code
→ It then uses it to execute templates
→ Those templates generate new source code
→ Your project can use this generated code
26
End of
Sourcery 101
27
Back to
writing tests
28
func testEquality() {
let personA = Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true)
let personB = Person(firstName: "Charlie", lastName: "Webb", age: 11, hasDriverLicense: false, isAmerican: true)
XCTAssertEqual(personA, personB)
}
29
!
30
A human-readable diff would be nice
31
Incorrect age: expected 10, received 11
32
Diffing methods are a perfect example of boilerplate
33
internal extension Person {
func diff(against other: Person) -> String {
var result = [String]()
if self.firstName != other.firstName {
var diff = "Incorrect firstName: "
diff += "expected (self.firstName), "
diff += "received (other.firstName)"
result.append(diff)
}
if self.lastName != other.lastName {
var diff = "Incorrect lastName: "
diff += "expected (self.lastName), "
diff += "received (other.lastName)"
result.append(diff)
}
if self.age != other.age {
var diff = "Incorrect age: "
diff += "expected (self.age), "
diff += "received (other.age)"
result.append(diff)
}
if self.hasDriverLicense != other.hasDriverLicense {
var diff = "Incorrect hasDriverLicense: "
diff += "expected (self.hasDriverLicense), "
diff += "received (other.hasDriverLicense)"
result.append(diff)
}
if self.isAmerican != other.isAmerican {
var diff = "Incorrect isAmerican: "
diff += "expected (self.isAmerican), "
diff += "received (other.isAmerican)"
result.append(diff)
}
return result.joined(separator: ". ")
}
}
34
How about we use Sourcery to generate it?
35
1st – Phantom Protocol
36
protocol Diffable { }
extension Person: Diffable { }
37
2nd – Sourcery Template
38
{% for type in types.implementing.Diffable %}
{{ type.accessLevel }} extension {{ type.name }} {
func diff(against other: {{ type.name }}) -> String {
var result = [String]()
{% for variable in type.variables %}
if self.{{ variable.name }} != other.{{ variable.name }} {
var diff = "Incorrect {{ variable.name }}: "
diff += "expected (self.{{ variable.name }}), "
diff += "received (other.{{ variable.name }})"
result.append(diff)
}
{% endfor %}
return result.joined(separator: ". ")
}
}
{% endfor %}
39
3rd – Generated Code
40
internal extension Person {
func diff(against other: Person) -> String {
var result = [String]()
if self.firstName != other.firstName {
var diff = "Incorrect firstName: "
diff += "expected (self.firstName), "
diff += "received (other.firstName)"
result.append(diff)
}
if self.lastName != other.lastName {
var diff = "Incorrect lastName: "
diff += "expected (self.lastName), "
diff += "received (other.lastName)"
result.append(diff)
}
if self.age != other.age {
var diff = "Incorrect age: "
diff += "expected (self.age), "
diff += "received (other.age)"
result.append(diff)
}
if self.hasDriverLicense != other.hasDriverLicense {
var diff = "Incorrect hasDriverLicense: "
diff += "expected (self.hasDriverLicense), "
diff += "received (other.hasDriverLicense)"
result.append(diff)
}
if self.isAmerican != other.isAmerican {
var diff = "Incorrect isAmerican: "
diff += "expected (self.isAmerican), "
diff += "received (other.isAmerican)"
result.append(diff)
}
return result.joined(separator: ". ")
}
}
41
4th – Updated Tests
42
let personA = Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true)
let personB = Person(firstName: "Charlie", lastName: "Webb", age: 11, hasDriverLicense: false, isAmerican: true)
XCTAssertEqual(personA, personB, personA.diff(against: personB))
43
!
44
How about we craft more complex tools?
45
Classic MVVM Architecture
protocol UserService {
func fetchUserName() -> String
}
class ViewModel {
var userNameUpdated: ((String) -> Void)?
private let service: UserService
init(service: UserService) {
self.service = service
}
func fetchData() {
let userName = self.service.fetchUserName()
self.userNameUpdated?(userName)
}
}
46
How do we test a component with dependencies?
Obvious, just inject mocked dependencies!
47
How do we write mocked dependencies?
How about we just don't? (And let Sourcery do it)
48
Generating Mocked Implementations
49
1st – Phantom Protocol
50
protocol MockedImplementation { }
protocol UserService: MockedImplementation {
func fetchUserName() -> String
}
51
2nd – Sourcery Template
52
{% for protocol in types.implementing.MockedImplementation|protocol %}
{{ protocol.accessLevel }} class Mocked{{ protocol.name }}: {{ protocol.name }} {
{% for method in protocol.methods %}
var {{ method.callName }}CallCounter: Int = 0
var {{ method.callName }}ReturnValue: {{ method.returnTypeName }}?
func {{ method.name }} -> {{ method.returnTypeName }} {
{{ method.callName }}CallCounter += 1
return {{ method.callName }}ReturnValue!
}
{% endfor %}
}
{% endfor %}
53
3rd – Generated Code
54
internal class MockedUserService: UserService {
var fetchUserNameCallCounter: Int = 0
var fetchUserNameReturnValue: String?
func fetchUserName() -> String {
fetchUserNameCallCounter += 1
return fetchUserNameReturnValue!
}
}
55
4th – Writing Tests
56
class ViewModelTests: XCTestCase {
func testUserServiceCalls() {
let mockedService = MockedUserService()
let viewModel = ViewModel(service: mockedService)
mockedService.fetchUserNameReturnValue = "John Appleseed"
viewModel.fetchData()
XCTAssertEqual(mockedService.fetchUserNameCallCounter, 1)
}
}
57
No more boilerplate
58
No more boilerplate
Now we only focus on writing tests for the business logic
Of course, there's a lot more features we could add:
→ variables to store arguments
→ calling completion handlers
→ dealing with throwing functions
→ etc.
59
No more boilerplate
Sourcery actually ships with a template that takes
care of all those needs: AutoMockable
(But beware, it is MUCH harder to understand )
60
We are now able to generate tools for testing...
61
...but there's still room to go even further!
62
Testing Dependency Injection
63
Dependency Injection
Many apps rely on Swinject to provide the
architectural basis of dependency injection.
64
Dependency Injection
import Swinject
class ViewModelAssembly: Assembly {
func assemble(container: Container) {
container.register(UserService.self) { _ in return ImplUserService() }
container.register(ViewModel.self) { resolver in
let service = resolver.resolve(UserService.self)!
return ViewModel(service: service)
}
}
}
65
Dependency Injection
class GreetingsViewControllerAssembly: Assembly {
func assemble(container: Container) {
container.register(GreetingsViewController.self) { resolver in
let viewModel = resolver.resolve(ViewModel.self)!
let viewController = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "GreetingsViewController")
as! GreetingsViewController
viewController.viewModel = viewModel
return viewController
}
}
}
66
Dependency Injection
struct ViewControllerFactory {
static var greetingsVC: GreetingsViewController {
let assembler = Assembler([ViewModelAssembly(), GreetingsViewControllerAssembly()])
return assembler.resolver.resolve(GreetingsViewController.self)!
}
}
67
Swinject relies exclusively on runtime checks
68
So we need tests, to avoid production crashes
69
How do we test those injections?
70
How do we test those injections?
Let's reason about the situation:
→ The dependencies follow a tree structure
→ The view controllers are the roots of those trees
→ By instantiating them, we trigger the whole
injection process
71
How do we test those injections?
Conclusion: we need to write tests that attempt to
instantiate all the controllers.
72
73
Sourcery Template
import XCTest
@testable import YourApp
class DependencyInjectionTests: XCTestCase {
func testDependencyInjection() {
{% for variable in type["ViewControllerFactory"].staticVariables %}
_ = ViewControllerFactory.{{ variable.name }}
{% endfor %}
}
}
74
Generated Code
import XCTest
@testable import TestSourcery
class DependencyInjectionTests: XCTestCase {
func testDependencyInjection() {
_ = ViewControllerFactory.greetingsVC
}
}
75
That's it!
As new controllers are added to the factory, the
corresponding tests will be written automatically
No more room for mistakes, pretty cool!
76
Recap
77
Recap
→ Sourcery is really easy to set up, don't feel scared to try it!
→ (It's also easy to take it out of a project, should you need)
→ It's a great tool to avoid writing boilerplate code by hand
→ Tests are a perfect place to use Sourcery, because they
involve lots of boilerplate
→ Every time a "one-size-fits-all" approach makes sense,
there's a good chance Sourcery can help
78
Don't rely on Humans
!"
to do the job of a Robot
79
Pro Tips
80
Be careful!
81
Be careful!
Sourcery lets us manipulate familiar concepts
(types, protocols, etc.) in an unfamiliar manner.
We get to look at our code the same way that Xcode
does.
This is not an approach we are used to, and it is very
easy to fail to consider some edge cases
82
First, ask Google
83
First, ask Google
Templates tend to be much more complicated than
initially thought (remember AutoMockable).
A little Google search might just save you a lot of
time by pointing you in the right direction.
84
Should I commit generated files?
85
Should I commit generated files?
We might initially think that generated code should not be
versionned.
However, you should seriously consider versionning it.
This way, changes to generated files will appear during code review,
providing the opportunity to check that everything still works fine.
If you don't version generated files, it becomes really easy to forget
that they even exist...
86
!
87
Taking the boilerplate out of
your tests with Sourcery
Vincent Pradeilles @v_pradeilles – Worldline

Taking the boilerplate out of your tests with Sourcery

  • 1.
    Taking the boilerplateout of your tests with Sourcery Vincent Pradeilles (@v_pradeilles) – Worldline !
  • 2.
  • 3.
  • 4.
    import XCTest class MyTests:XCTestCase { func testExample() { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } } 4
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
    What is Sourcery? "Sourceryis a code generator for the Swift language, built on top of Apple's own SourceKit. It extends the language abstractions to allow you to generate boilerplate code automatically."1 1  https://github.com/krzysztofzablocki/Sourcery/blob/master/README.md 9
  • 10.
    What can wedo with Sourcery? 10
  • 11.
    What can wedo with Sourcery? enum Direction { case up case right case down case left } Direction.allCases // [.up, .right, .down, .left] 11
  • 12.
    What can wedo with Sourcery? Since Swift 4.2, this can be achieved through the CaseIterable protocol. But before, generating .allCases was a great use case to illustrate how Sourcery works. 12
  • 13.
  • 14.
    1st – SettingUp Sourcery 14
  • 15.
    Setting Up Sourcery $brew install sourcery 15
  • 16.
    Setting Up Sourcery XcodeProject > Build Phases > New Run Script Phase sourcery --sources <sources path> --templates <templates path> --output <output path> 16
  • 17.
    2nd – PhantomProtocol 17
  • 18.
    Phantom Protocol protocol EnumIterable{ } extension Direction: EnumIterable { } 18
  • 19.
    3rd – SourceryTemplate 19
  • 20.
    Sourcery Template {% forenum in types.implementing.EnumIterable|enum %} {% if not enum.hasAssociatedValues %} {{ enum.accessLevel }} extension {{ enum.name }} { static let allCases: [{{ enum.name }}] = [ {% for case in enum.cases %} .{{ case.name }} {% if not forloop.last %} , {% endif %} {% endfor %} ] } {% endif %} {% endfor %} 20
  • 21.
  • 22.
  • 23.
    Generated Code internal extensionDirection { static let allCases: [Direction] = [ .up , .right , .down , .left ] } 23
  • 24.
    Generated Code Add thegenerated file to your project That's it ! 24
  • 25.
  • 26.
    To Sum Up →Sourcery parses your source code → It then uses it to execute templates → Those templates generate new source code → Your project can use this generated code 26
  • 27.
  • 28.
  • 29.
    func testEquality() { letpersonA = Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true) let personB = Person(firstName: "Charlie", lastName: "Webb", age: 11, hasDriverLicense: false, isAmerican: true) XCTAssertEqual(personA, personB) } 29
  • 30.
  • 31.
    A human-readable diffwould be nice 31
  • 32.
    Incorrect age: expected10, received 11 32
  • 33.
    Diffing methods area perfect example of boilerplate 33
  • 34.
    internal extension Person{ func diff(against other: Person) -> String { var result = [String]() if self.firstName != other.firstName { var diff = "Incorrect firstName: " diff += "expected (self.firstName), " diff += "received (other.firstName)" result.append(diff) } if self.lastName != other.lastName { var diff = "Incorrect lastName: " diff += "expected (self.lastName), " diff += "received (other.lastName)" result.append(diff) } if self.age != other.age { var diff = "Incorrect age: " diff += "expected (self.age), " diff += "received (other.age)" result.append(diff) } if self.hasDriverLicense != other.hasDriverLicense { var diff = "Incorrect hasDriverLicense: " diff += "expected (self.hasDriverLicense), " diff += "received (other.hasDriverLicense)" result.append(diff) } if self.isAmerican != other.isAmerican { var diff = "Incorrect isAmerican: " diff += "expected (self.isAmerican), " diff += "received (other.isAmerican)" result.append(diff) } return result.joined(separator: ". ") } } 34
  • 35.
    How about weuse Sourcery to generate it? 35
  • 36.
    1st – PhantomProtocol 36
  • 37.
    protocol Diffable {} extension Person: Diffable { } 37
  • 38.
    2nd – SourceryTemplate 38
  • 39.
    {% for typein types.implementing.Diffable %} {{ type.accessLevel }} extension {{ type.name }} { func diff(against other: {{ type.name }}) -> String { var result = [String]() {% for variable in type.variables %} if self.{{ variable.name }} != other.{{ variable.name }} { var diff = "Incorrect {{ variable.name }}: " diff += "expected (self.{{ variable.name }}), " diff += "received (other.{{ variable.name }})" result.append(diff) } {% endfor %} return result.joined(separator: ". ") } } {% endfor %} 39
  • 40.
  • 41.
    internal extension Person{ func diff(against other: Person) -> String { var result = [String]() if self.firstName != other.firstName { var diff = "Incorrect firstName: " diff += "expected (self.firstName), " diff += "received (other.firstName)" result.append(diff) } if self.lastName != other.lastName { var diff = "Incorrect lastName: " diff += "expected (self.lastName), " diff += "received (other.lastName)" result.append(diff) } if self.age != other.age { var diff = "Incorrect age: " diff += "expected (self.age), " diff += "received (other.age)" result.append(diff) } if self.hasDriverLicense != other.hasDriverLicense { var diff = "Incorrect hasDriverLicense: " diff += "expected (self.hasDriverLicense), " diff += "received (other.hasDriverLicense)" result.append(diff) } if self.isAmerican != other.isAmerican { var diff = "Incorrect isAmerican: " diff += "expected (self.isAmerican), " diff += "received (other.isAmerican)" result.append(diff) } return result.joined(separator: ". ") } } 41
  • 42.
  • 43.
    let personA =Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true) let personB = Person(firstName: "Charlie", lastName: "Webb", age: 11, hasDriverLicense: false, isAmerican: true) XCTAssertEqual(personA, personB, personA.diff(against: personB)) 43
  • 44.
  • 45.
    How about wecraft more complex tools? 45
  • 46.
    Classic MVVM Architecture protocolUserService { func fetchUserName() -> String } class ViewModel { var userNameUpdated: ((String) -> Void)? private let service: UserService init(service: UserService) { self.service = service } func fetchData() { let userName = self.service.fetchUserName() self.userNameUpdated?(userName) } } 46
  • 47.
    How do wetest a component with dependencies? Obvious, just inject mocked dependencies! 47
  • 48.
    How do wewrite mocked dependencies? How about we just don't? (And let Sourcery do it) 48
  • 49.
  • 50.
    1st – PhantomProtocol 50
  • 51.
    protocol MockedImplementation {} protocol UserService: MockedImplementation { func fetchUserName() -> String } 51
  • 52.
    2nd – SourceryTemplate 52
  • 53.
    {% for protocolin types.implementing.MockedImplementation|protocol %} {{ protocol.accessLevel }} class Mocked{{ protocol.name }}: {{ protocol.name }} { {% for method in protocol.methods %} var {{ method.callName }}CallCounter: Int = 0 var {{ method.callName }}ReturnValue: {{ method.returnTypeName }}? func {{ method.name }} -> {{ method.returnTypeName }} { {{ method.callName }}CallCounter += 1 return {{ method.callName }}ReturnValue! } {% endfor %} } {% endfor %} 53
  • 54.
  • 55.
    internal class MockedUserService:UserService { var fetchUserNameCallCounter: Int = 0 var fetchUserNameReturnValue: String? func fetchUserName() -> String { fetchUserNameCallCounter += 1 return fetchUserNameReturnValue! } } 55
  • 56.
  • 57.
    class ViewModelTests: XCTestCase{ func testUserServiceCalls() { let mockedService = MockedUserService() let viewModel = ViewModel(service: mockedService) mockedService.fetchUserNameReturnValue = "John Appleseed" viewModel.fetchData() XCTAssertEqual(mockedService.fetchUserNameCallCounter, 1) } } 57
  • 58.
  • 59.
    No more boilerplate Nowwe only focus on writing tests for the business logic Of course, there's a lot more features we could add: → variables to store arguments → calling completion handlers → dealing with throwing functions → etc. 59
  • 60.
    No more boilerplate Sourceryactually ships with a template that takes care of all those needs: AutoMockable (But beware, it is MUCH harder to understand ) 60
  • 61.
    We are nowable to generate tools for testing... 61
  • 62.
    ...but there's stillroom to go even further! 62
  • 63.
  • 64.
    Dependency Injection Many appsrely on Swinject to provide the architectural basis of dependency injection. 64
  • 65.
    Dependency Injection import Swinject classViewModelAssembly: Assembly { func assemble(container: Container) { container.register(UserService.self) { _ in return ImplUserService() } container.register(ViewModel.self) { resolver in let service = resolver.resolve(UserService.self)! return ViewModel(service: service) } } } 65
  • 66.
    Dependency Injection class GreetingsViewControllerAssembly:Assembly { func assemble(container: Container) { container.register(GreetingsViewController.self) { resolver in let viewModel = resolver.resolve(ViewModel.self)! let viewController = UIStoryboard(name: "Main", bundle: nil) .instantiateViewController(withIdentifier: "GreetingsViewController") as! GreetingsViewController viewController.viewModel = viewModel return viewController } } } 66
  • 67.
    Dependency Injection struct ViewControllerFactory{ static var greetingsVC: GreetingsViewController { let assembler = Assembler([ViewModelAssembly(), GreetingsViewControllerAssembly()]) return assembler.resolver.resolve(GreetingsViewController.self)! } } 67
  • 68.
    Swinject relies exclusivelyon runtime checks 68
  • 69.
    So we needtests, to avoid production crashes 69
  • 70.
    How do wetest those injections? 70
  • 71.
    How do wetest those injections? Let's reason about the situation: → The dependencies follow a tree structure → The view controllers are the roots of those trees → By instantiating them, we trigger the whole injection process 71
  • 72.
    How do wetest those injections? Conclusion: we need to write tests that attempt to instantiate all the controllers. 72
  • 73.
  • 74.
    Sourcery Template import XCTest @testableimport YourApp class DependencyInjectionTests: XCTestCase { func testDependencyInjection() { {% for variable in type["ViewControllerFactory"].staticVariables %} _ = ViewControllerFactory.{{ variable.name }} {% endfor %} } } 74
  • 75.
    Generated Code import XCTest @testableimport TestSourcery class DependencyInjectionTests: XCTestCase { func testDependencyInjection() { _ = ViewControllerFactory.greetingsVC } } 75
  • 76.
    That's it! As newcontrollers are added to the factory, the corresponding tests will be written automatically No more room for mistakes, pretty cool! 76
  • 77.
  • 78.
    Recap → Sourcery isreally easy to set up, don't feel scared to try it! → (It's also easy to take it out of a project, should you need) → It's a great tool to avoid writing boilerplate code by hand → Tests are a perfect place to use Sourcery, because they involve lots of boilerplate → Every time a "one-size-fits-all" approach makes sense, there's a good chance Sourcery can help 78
  • 79.
    Don't rely onHumans !" to do the job of a Robot 79
  • 80.
  • 81.
  • 82.
    Be careful! Sourcery letsus manipulate familiar concepts (types, protocols, etc.) in an unfamiliar manner. We get to look at our code the same way that Xcode does. This is not an approach we are used to, and it is very easy to fail to consider some edge cases 82
  • 83.
  • 84.
    First, ask Google Templatestend to be much more complicated than initially thought (remember AutoMockable). A little Google search might just save you a lot of time by pointing you in the right direction. 84
  • 85.
    Should I commitgenerated files? 85
  • 86.
    Should I commitgenerated files? We might initially think that generated code should not be versionned. However, you should seriously consider versionning it. This way, changes to generated files will appear during code review, providing the opportunity to check that everything still works fine. If you don't version generated files, it becomes really easy to forget that they even exist... 86
  • 87.
  • 88.
    Taking the boilerplateout of your tests with Sourcery Vincent Pradeilles @v_pradeilles – Worldline