Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
14 views

iOS Test Driven Development by Tutorials v2.0.0

Uploaded by

haes.millos
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
14 views

iOS Test Driven Development by Tutorials v2.0.0

Uploaded by

haes.millos
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 332

iOS Test-Driven Development by Tutorials iOS Test-Driven Development by Tutorials

iOS Test-Driven Development by Tutorials


By Joshua Greene & Michael Katz

Copyright ©2022 Razeware LLC.

Notice of Rights
All rights reserved. No part of this book or corresponding materials (such as text,
images, or source code) may be reproduced or distributed by any means without
prior written permission of the copyright owner.

Notice of Liability
This book and all corresponding materials (such as source code) are provided on an
“as is” basis, without warranty of any kind, express of implied, including but not
limited to the warranties of merchantability, fitness for a particular purpose, and
noninfringement. In no event shall the authors or copyright holders be liable for any
claim, damages or other liability, whether in action of contract, tort or otherwise,
arising from, out of or in connection with the software or the use of other dealing in
the software.

Trademarks
All trademarks and registered trademarks appearing in this book are the property of
their own respective owners.

raywenderlich.com 2
iOS Test-Driven Development by Tutorials

Table of Contents: Overview


Book License ............................................................................................. 11
Before You Begin ................................................................ 12
What You Need ........................................................................................ 13
Book Source Code & Forums ............................................................. 14
Introduction .............................................................................................. 17
Section I: Hello, TDD! ........................................................ 20
Chapter 1: What Is TDD?......................................................... 21
Chapter 2: The TDD Cycle ...................................................... 26
Section II: Beginning TDD ................................................ 41
Chapter 3: TDD App Setup ..................................................... 42
Chapter 4: Test Expressions ................................................... 62
Chapter 5: Test Expectations ................................................. 85
Chapter 6: Dependency Injection & Mocks .................. 112
Section III: TDD With Networking ............................. 137
Chapter 7: Introducing Dog Patch .................................... 138
Chapter 8: RESTful Networking ........................................ 142
Chapter 9: Using the Network Client .............................. 177
Chapter 10: ImageClient....................................................... 195
Section IV: TDD in Legacy Apps .................................. 230
Chapter 11: Legacy Problems ............................................. 231
Chapter 12: Dependency Maps ......................................... 257

raywenderlich.com 3
iOS Test-Driven Development by Tutorials

Chapter 13: Breaking Up Dependencies ........................ 273


Chapter 14: Modularizing Dependencies ...................... 295
Chapter 15: Adding Features to Existing Classes ....... 313
Conclusion .............................................................................................. 332

raywenderlich.com 4
iOS Test-Driven Development by Tutorials

Table of Contents: Extended


Book License . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
Before You Begin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
What You Need . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
Book Source Code & Forums . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
About the Authors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
About the Editors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
About this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
Section introductions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
How to read this book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

Section I: Hello, TDD! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20


Chapter 1: What Is TDD? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Why should you use TDD? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
What should you test? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
But TDD takes too long! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
When should you use TDD? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Chapter 2: The TDD Cycle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
Red: Write a failing test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Green: Make the test pass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Refactor: Clean up your code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Repeat: Do it again . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
TDDing init(availableFunds:) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
TDDing addItem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Adding two items . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

raywenderlich.com 5
iOS Test-Driven Development by Tutorials

Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40

Section II: Beginning TDD . . . . . . . . . . . . . . . . . . . . . . . . . . 41


Chapter 3: TDD App Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
About the FitNess app . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Your first test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Red-Green-Refactor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Test nomenclature . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Structure of XCTestCase subclass . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Your next set of tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
Using @testable import . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
Testing UI updates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Where to go from here?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
Chapter 4: Test Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
Assert methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
View controller testing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
Test ordering matters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Code coverage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Debugging tests. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
Where to go from here?. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
Chapter 5: Test Expectations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Using an expectation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
Testing for true asynchronicity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Waiting for notifications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Showing the alert to a user . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95

raywenderlich.com 6
iOS Test-Driven Development by Tutorials

Getting specific about notifications . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101


Driving alerts from the data model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
Using other types of expectations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Chapter 6: Dependency Injection & Mocks . . . . . . . . . . . . . . . . 112
What’s up with fakes, mocks, and stubs? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Understanding CMPedometer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
Mocking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
Handling error conditions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Getting actual data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
Making a functional fake . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
Wiring up the chase view . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
Time dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

Section III: TDD With Networking . . . . . . . . . . . . . . . . 137


Chapter 7: Introducing Dog Patch. . . . . . . . . . . . . . . . . . . . . . . . . . 138
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
Understanding Dog Patch’s architecture. . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141
Chapter 8: RESTful Networking . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
Setting up the networking client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
TDDing the networking call . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
Chapter 9: Using the Network Client . . . . . . . . . . . . . . . . . . . . . . 177

raywenderlich.com 7
iOS Test-Driven Development by Tutorials

Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177


Creating a shared instance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
Adding a network client property . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
Using the network client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
Creating the network client protocol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
Creating the mock network client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
Using the mock network client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
Chapter 10: ImageClient . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Setting up the image client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196
Creating an image client protocol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
Downloading an image . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
Caching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213
Setting an image view from a URL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
Using the image client . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 229

Section IV: TDD in Legacy Apps . . . . . . . . . . . . . . . . . . . 230


Chapter 11: Legacy Problems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
Introducing MyBiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232
Identifying a change point . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Finding a test point . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
Breaking dependencies. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
Writing tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
Making a change and refactoring. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 250
Challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Chapter 12: Dependency Maps . . . . . . . . . . . . . . . . . . . . . . . . . . . . 257
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258

raywenderlich.com 8
iOS Test-Driven Development by Tutorials

Choosing where to begin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258


Finding direct dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
Finding secondary dependencies. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
Deciding when to stop . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 266
What are problematic dependencies? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Finding problematic dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 267
Completing the map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Breaking up complex systems . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
Chapter 13: Breaking Up Dependencies . . . . . . . . . . . . . . . . . . . 273
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
Characterizing the system . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Breaking up the API/AppDelegate dependency. . . . . . . . . . . . . . . . . . . . . 278
Breaking the AppDelegate dependency . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
Breaking the ErrorViewController dependency . . . . . . . . . . . . . . . . . . . . 287
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
Key Points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 294
Chapter 14: Modularizing Dependencies . . . . . . . . . . . . . . . . . . 295
Moving files. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 297
Using the new framework with Login . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302
Fixing MyBiz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 305
Wrap up . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 310
Challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 312
Chapter 15: Adding Features to Existing Classes . . . . . . . . . . 313
Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 313
Sending reports . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 314

raywenderlich.com 9
iOS Test-Driven Development by Tutorials

Adding analytics to the view controllers . . . . . . . . . . . . . . . . . . . . . . . . . . . . 322


Passing around dependencies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 325
Challenge . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Key points . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330
Where to go from here? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331
Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332

raywenderlich.com 10
L Book License

By purchasing iOS Test-Driven Development by Tutorials, you have the following


license:

• You are allowed to use and/or modify the source code in iOS Test-Driven
Development by Tutorials in as many apps as you want, with no attribution
required.

• You are allowed to use and/or modify all art, images and designs that are included
in this book in as many apps as you want, but must include this attribution line
somewhere inside your app: “Artwork/images/designs: from iOS Test-Driven
Development by Tutorials, available at www.raywenderlich.com”.

• The source code included in iOS Test-Driven Development by Tutorials is for your
personal use only. You are NOT allowed to distribute or sell the source code in iOS
Test-Driven Development by Tutorials without prior authorization.

• This book is for your personal use only. You are NOT allowed to sell this book
without prior authorization, or distribute it to friends, coworkers or students; they
would need to purchase their own copies.

All materials provided with this book are provided on an “as is” basis, without
warranty of any kind, express or implied, including but not limited to the warranties
of merchantability, fitness for a particular purpose and noninfringement. In no event
shall the authors or copyright holders be liable for any claim, damages or other
liability, whether in an action or contract, tort or otherwise, arising from, out of or in
connection with the software or the use or other dealings in the software.

All trademarks and registered trademarks appearing in this guide are the properties
of their respective owners.

raywenderlich.com 11
Before You Begin

This section tells you a few things you need to know before you get started, such as
what you’ll need for hardware and software, where to find the project files for this
book, and more.

raywenderlich.com 12
i What You Need

To follow along with this book, you’ll need the following:

• Xcode 13 or later. Xcode is the main development tool for writing code in Swift.
You need Xcode 13 at a minimum, since that version includes Swift 5.5. You can
download the latest version of Xcode for free from the Mac App Store, here:
apple.co/1FLn51R.

If you haven’t installed the latest version of Xcode, be sure to do that before
continuing with the book. The code covered in this book depends on Swift 5.5 and
Xcode 13 — the code may not compile if you try to work with an older version.

raywenderlich.com 13
ii Book Source Code &
Forums

Where to download the materials for this book


The materials for this book can be cloned or downloaded from the GitHub book
materials repository:

• https://github.com/raywenderlich/itdd-materials/tree/editions/2.0

Forums
We’ve also set up an official forum for the book at https://
forums.raywenderlich.com/c/books/ios-test-driven-development-by-tutorials. This
is a great place to ask questions about the book or to submit any errors you may find.

raywenderlich.com 14
Dedications
“For my girls. I love you very much.”

— Joshua Greene

“Dedicated to the memory of my mother-in-law, Barbara


Schwartz. Her selflessness and dedication to teaching inspires
me to give back to the community and educate others.”

— Michael Katz

raywenderlich.com 15
iOS Test-Driven Development by Tutorials About the Team

About the Authors


Joshua Greene is an author of this book. He’s an experienced
software engineer and long-time member of the
raywenderlich.com team. He’s created dozens of mobile apps,
several books and hundreds of videos and tutorials about software
development. When he’s not slinging code, you can find him
wandering the streets of Tokyo. You can reach him on Twitter
@jrg_developer (https://twitter.com/jrg_developer).

Michael Katz is a champion baker. ;] Oh, he’s also an author of


this book, developer, architect, speaker, writer and avid
homebrewer. He has contributed to several books on iOS
development and is a long-time member of the raywenderlich.com
tutorial team. He shares his home state of New York with his
family, the world’s best bagels and the Yankees. When he’s not at
his computer, he’s out on the trails, in his shop or reading a good
book (like this one!).

About the Editors


Darren Ferguson is the final pass editor for this book. He is an
experienced software developer and works for M.C. Dean, Inc, a
systems integration provider from North Virginia. When he’s not
coding, you’ll find him enjoying EPL Football, traveling as much as
possible and spending time with his wife and daughter.

April Rames is the editor of this book. April is a former high


school English and theatre teacher and director. When not
volunteering at her daughters’ schools, she usually spends her time
being asked to pretend to be a unicorn, zombie princess or super
hero. In her spare time, she enjoys reading, making pasta and
exploring the Gulf Coast with her family.

raywenderlich.com 16
v Introduction

Welcome to iOS Test-Driven Development by Tutorials! This book will teach you all
about test-driven development (TDD) — the art of turning requirements into tests
and tests into production code.

You’ll get hands-on TDD experience by creating three real-world apps in this book:

By the end of this book, you’ll have a strong understanding of TDD and be able to
apply this knowledge to your own apps.

raywenderlich.com 17
iOS Test-Driven Development by Tutorials Introduction

About this book


We wrote this book with beginner-to-intermediate developers in mind. The only
requirements for reading this book are a basic understanding of Swift and iOS
development.

If you’ve worked through our classic beginner books — the Swift Apprentice https://
www.raywenderlich.com/books/swift-apprentice and the UIKit Apprentice https://
www.raywenderlich.com/books/uikit-apprentice — or have similar development
experience, you’re ready to read this book. You’ll also benefit from a working
knowledge of design patterns — such as working through Design Patterns by Tutorials
https://www.raywenderlich.com/books/design-patterns-by-tutorials — but this isn’t
strictly required.

As you work through this book, you’ll progress from beginner topics to more
advanced concepts.

Section introductions
I. Introduction
This is a high-level introduction to TDD, explaining why it’s important and how it
will help you.

You’ll also be introduced to the TDD Cycle in this section. This is the foundation for
how TDD works and guiding principles on the best way to apply it.

II. Beginning TDD


You’ll learn the basics of TDD in this section, including XCTest, test expressions,
mocks and test expectations.

The chapters in this section build an example app called Fitness. This is the premier
fitness-coaching app based on the “Loch Ness” workout: You’ll have to outrun,
outswim and outclimb Nessie (or get eaten)!

raywenderlich.com 18
iOS Test-Driven Development by Tutorials Introduction

III. TDD with Networking


You’ll learn about TDD and networking in this section, including writing tests for
RESTful networking calls, downloading images and using networking clients.

You’ll create an app called Dog Patch throughout this section. Dog Patch lets dog
lovers everywhere connect with kind breeders to help get the dog of their dreams.

IV. TDD in Legacy Apps


This section will teach you how to start TDD in a legacy app that wasn’t created with
TDD and doesn’t have sufficient test coverage.

You’ll update an app called MyBiz throughout this section. MyBiz is an enterprise
resource planning (ERP) app for running a business, including employee
management and scheduling, time tracking, payroll and inventory management.

How to read this book


If you’re new to unit testing or TDD, you should read this book from cover to cover.

If you already have some experience with TDD, you can skip from chapter to chapter
or use this book as a reference. You’ll always be provided with a starter project in
each chapter to get up and running quickly.

What’s the absolute best way to read this book? Just start reading wherever makes
sense to you!

raywenderlich.com 19
Section I: Hello, TDD!

This section is a high-level introduction to test-driven development, how it works


and why you should use it. You’ll also learn about the TDD cycle in this chapter, and
you’ll use this throughout the rest of the book.

raywenderlich.com 20
1 Chapter 1: What Is TDD?
By Joshua Greene

Test-driven development, or TDD, is an iterative way to create software by making


many small changes backed by tests.

It has four steps:

1. Write a failing test

2. Make the test pass

3. Refactor

4. Repeat

This is called the TDD Cycle. It ensures you thoroughly and accurately test your
code because your development is… driven by testing!

By writing a test followed by the production code to make it pass, you ensure your
production code is testable and that it meets all requirements during development.
As an added bonus, your tests act as documentation for your production code,
describing how it works.

raywenderlich.com 21
iOS Test-Driven Development by Tutorials Chapter 1: What Is TDD?

On the surface, the TDD process seems pretty simple. Well, I’m sorry to tell you
that… wait, it actually is really simple!

Sure, there are special circumstances for how to implement this cycle at times, but
that’s where this book comes in! Once you get the hang of this process, it will
become second nature. You’ll learn a lot more about this in the next chapter.

Why should you use TDD?


TDD is the single best way to ensure your software works and continues to work well
into the future — well, that’s quite a bold claim! Let me explain.

It’s hard to argue against testing your code, but you don’t have to follow TDD to do
this. For example, you could write all of your production code and then write all of
your tests. Alternatively, you could skip writing tests altogether and, instead,
manually test your code. Why is TDD better than these options?

Good tests ensure your app works as expected. However, not all tests are “good.”
Writing tests for the sake of having tests isn’t a worthwhile exercise. Rather, good
tests are failable, repeatable, quick to run and maintainable.

TDD provides methodology that ensures your tests are good:

• The first step is to write a failing test. By definition, this proves the test can fail.
Tests that can’t fail aren’t useful. Rather, they waste valuable CPU time.

• Before you’re allowed to write a new test, all previous tests must pass. This
ensures that your tests are repeatable: You don’t just run the single test you’re
writing, but rather, you constantly run all of the tests.

• By frequently running every test, you’re incentivized to make sure tests are quick
to run. All of your tests should take seconds to run — preferably, one second or
less.

raywenderlich.com 22
iOS Test-Driven Development by Tutorials Chapter 1: What Is TDD?

A single test that takes a hundred milliseconds is too slow: After only ten tests, your
entire test suite will take one second to run. After fifty tests, it takes five seconds.
After several seconds, no one runs all of the tests because it takes too long.

• When you refactor, you update both your production and test code. This ensures
your tests are maintained: You’re constantly keeping them up-to-date.

• By iteratively writing production code and tests in parallel, you ensure your code is
testable. If you were to write tests after completing the code, it’s likely the
production code would require many changes to fully unit test.

Nonetheless, the devil’s advocate in you may say, “But you could write good tests
without following TDD.” You definitely could, but you may struggle to succeed. You
can definitely do it in the short term, but it’s much more difficult in the long term.
You’d need to be disciplined about writing good tests. Before long, you’d likely create
some sort of system to ensure that you’re writing good tests… you’d likely find
yourself doing a variant of TDD!

What should you test?


Better test coverage doesn’t always mean your app is better tested. There are things
you should test and others you shouldn’t. Here are the do’s and don’ts:

• Do write tests for code that can’t be caught in an automated fashion otherwise.
This includes code in your classes’ methods, custom getters and setters and mostly
anything else you write yourself.

• Don’t write tests for generated code. For example, it’s not worthwhile to write
tests for generated getters and setters. Swift does this very well, and you can trust
it works.

• Don’t write tests for issues that can be caught by the compiler. If the tested issue
would generate an error or warning, Xcode will catch it for you.

• Don’t write tests for dependency code, such as first- or third-party frameworks
your app uses. The framework authors are responsible for writing those tests. For
example, you shouldn’t write tests for core Foundation classes because Apple’s
developers are responsible for writing those. However, you should write tests for
your custom subclasses thereof: This is your custom code, so you’re responsible
for writing the tests.

raywenderlich.com 23
iOS Test-Driven Development by Tutorials Chapter 1: What Is TDD?

An exception to the above is writing tests in order to determine how a framework


works. This can be very useful to do. However, you don’t need to keep these tests
long term. Rather, you should delete them afterwards.

Another exception is “sanity tests” that prove third-party code works as you expect.
These sort of tests are useful if the library isn’t fully stable, or you don’t trust it
entirely. In either case, you should really scrutinize whether or not you want to use
the library at all — is there a better option that’s more trustworthy?

But TDD takes too long!


The most common complaint about TDD is that it takes too long — usually followed
by exclamation point(s) or sad-face emojis.

Fortunately, TDD gets faster once you get used to doing it. However, the truth is that
compared to not writing any tests at all, you’re writing more code ultimately. It likely
will take a little more time to develop initially.

That said, there’s a really big hole in this argument: The real time cost of
development isn’t just writing the initial, first-version production code. It also
includes adding new features over time, modifying existing code, fixing bugs and
more. In the long run, following TDD takes much less time than not following it
because it yields more maintainable code with fewer bugs.

There’s also another cost to consider: customer impact of bugs in production. The
longer an issue goes undiscovered, the more expensive it is. It can result in negative
reviews, lost trust and lost revenue.

If an issue is caught during development, it’s easier to debug and quicker to fix. If
you discovered it weeks later, you’d spend substantially more time getting up to
speed on the code and tracking down the root cause. By following TDD, your tests
ultimately help safeguard and protect your app against bugs.

raywenderlich.com 24
iOS Test-Driven Development by Tutorials Chapter 1: What Is TDD?

When should you use TDD?


TDD can be used during any point in a product’s life cycle: new development, legacy
apps and everything in between. However, how and where you start TDD does
depend on the state of your project. This book will cover how to approach many of
these situations!

However, an important question to ask: Should your project use TDD at all?

As a general rule of thumb, if your app is going to last more than a few months, will
have multiple releases and/or require complex logic, you’re likely better off using
TDD than not.

If you’re creating an app for a hackathon, test project or something else that’s meant
to be temporary, you should evaluate whether TDD makes sense. If there’s really only
going to be one version of the app, you might not follow TDD or might only do TDD
for critical or difficult parts.

Ultimately, TDD is a tool, and it’s up to you to decide when it’s best to use it!

Key points
In this chapter, you learned what TDD is, why you should use it, what to test and
when to use it. Here are the key points to remember:

• TDD offers a consistent method to write good tests.

• Goods tests are failable, repeatable, quick to run and maintainable.

• Write tests for code that you’re responsible for maintaining. Don’t test code that’s
automatically generated or code within dependencies.

• The real cost of development includes initial coding time, adding new features
over time, modifying existing code, fixing bugs and more. TDD reduces
maintenance costs and quantity of bugs, often making it the most cost effective
approach.

• TDD is most useful for long-term projects lasting more than a few months or
having multiple releases.

raywenderlich.com 25
2 Chapter 2: The TDD Cycle
By Joshua Greene

In the previous chapter, you learned that test-driven development boils down to a
simple process called the TDD Cycle. It has four steps that are often “color coded” as
follows:

1. Red: Write a failing test, before writing any app code.

2. Green: Write the bare minimum code to make the test pass.

3. Refactor: Clean up both your app and test code.

4. Repeat: Do this cycle again until all features are implemented.

This is also called the Red-Green-Refactor Cycle.

raywenderlich.com 26
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Why is it color coded? This corresponds to the colors shown in most code editors,
including Xcode:

• Failing tests are indicated with a red X.

• Passing tests are shown with a green checkmark.

This chapter provides an introduction to the TDD Cycle, which you’ll use throughout
the rest of this book. However, it doesn’t go into detail about test expressions
(XCTAssert, et al.) or how to set up a test target. Rather, these topics are covered in
later chapters. For now, focus on learning the TDD Cycle, and you’ll learn the rest as
you go along.

It’s best to learn by doing, so let’s jump straight into code!

Getting started
In this chapter, you’ll create a simple version of a cash register to learn the TDD
Cycle. To keep the focus on TDD instead of Xcode setup, you’ll use a playground.
Open CashRegister.playground in the starter directory, then open the
CashRegister page. You’ll see this page two imports, but otherwise it’s empty.

Naturally, you’ll begin with the first step in the TDD Cycle: red.

raywenderlich.com 27
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Red: Write a failing test


Before you write any production code, you must first write a failing test. To do so,
you need to create a test class. Add the following below the import statements:

class CashRegisterTests: XCTestCase {

Above, you declare CashRegisterTests as a subclass of XCTestCase, which is part


of the XCTest framework. You’ll almost always subclass XCTestCase to create your
test classes.

Next, add the following at the end of the playground:

CashRegisterTests.defaultTestSuite.run()

This tells the playground to run the test methods defined within
CashRegisterTests. However, you haven’t actually written any tests yet. Add the
following within CashRegisterTests, which should cause a compiler error:

// 1
func testInit_createsCashRegister() {
// 2
XCTAssertNotNil(CashRegister())
}

Here’s a line-by-line explanation:

1. We name tests using this convention throughout the book:

• XCTest requires all test methods begin with the keyword test to be run.

• Next, describe what’s being tested. Here, this is init. There’s then an underscore
to seprate it from the next part.

• If special setup is required, it’s written next. For example, you might describe what
setup conditions are necessary for the test. This is optional, however, and this test
doesn’t actually include this part. If it were included, you’d likewise suffix it with
an underscore to separate it from the last part.

• Lastly, describe the expected outcome or result. Here, this is


createsCashRegister.

raywenderlich.com 28
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

This convention results in test names that are easy to read and provide meaningful
context. If a test ever fails, Xcode will tell you the name of the test’s class and
method. By naming your tests this way, you can quickly determine the problem.

2. You attempt to instantiate a new instance of CashRegister, which you pass into
XCTAssertNil. This is a test expression that asserts whatever passed to it is not
nil. If it actually is nil, the test will be marked as failed.

However, this last line doesn’t compile! This is because you haven’t created a class
for CashRegister just yet… how are you suppose to advance the TDD Cycle, then?
Fortunately, there’s a rule in TDD for this: Compilation failures count as test failures.
So, you’ve completed the red step in the TDD Cycle and can move onto the next step:
green.

Green: Make the test pass


You’re only allowed to write the bare minimum code to make a test pass. If you write
more code than this, your tests will fall behind your app code. What’s the bare
minimum code you can write to fix this compilation error? Define CashRegister!

Add the following directly above class CashRegisterTests:

class CashRegister {

Press Play to execute the playground, and you should see output similar to the
following in the console:

Test Suite 'CashRegisterTests' started at


2021-07-22 16:55:35.336
Test Case '-[__lldb_expr_3.CashRegisterTests
testInit_createsCashRegister]' started.
Test Case '-[__lldb_expr_3.CashRegisterTests
testInit_createsCashRegister]' passed (0.081 seconds).
Test Suite 'CashRegisterTests' passed at
2021-07-22 16:55:35.418.
Executed 1 test, with 0 failures (0 unexpected) in 0.081
(0.082) seconds

Awesome, you’ve made the test pass! The next step is to refactor your code.

raywenderlich.com 29
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Refactor: Clean up your code


You’ll clean up both your app code and test code in the refactor step. By doing so,
you constantly maintain and improve your code. Here are a few things you might
look to refactor:

• Duplicate logic: Can you pull out any properties, methods or classes to eliminate
duplication?

• Comments: Your comments should explain why something is done, not how it’s
done. Try to eliminate comments that explain how code works. The how should be
conveyed by breaking up large methods into several well-named methods,
renaming properties and methods to be more clear or sometimes simply
structuring your code better.

• Code smells: Sometimes a particular block of code simply seems wrong. Trust
your gut and try to eliminate these “code smells.” For example, you might have
logic that’s making too many assumptions, uses hardcoded strings or has other
issues. The tricks from above apply here, too: Pulling out methods and classes,
renaming and restructuring code can go a long way to fixing these problems.

Right now, CashRegister and CashRegisterTests don’t have much logic in them,
and there isn’t anything to refactor. So, you’re done with this step — that was easy!
The most important step in the TDD Cycle happens next: repeat.

Repeat: Do it again
Use TDD throughout your app’s development to get the most benefit from it. You’ll
accomplish a little bit in each TDD Cycle, and you’ll build up app code backed by
tests. Once you’ve completed all of your app’s features, you’ll have a working, well-
tested system.

You’ve completed your first TDD Cycle, and you now have a class that can be
instantiated: CashRegister. However, there’s still more functionality to add for this
class to be useful. Here’s your to-do list:

• Write an initializer that accepts availableFunds.

• Write a method for addItem that adds to a transaction.

• Write a method for acceptPayment.

You’ve got this!

raywenderlich.com 30
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

TDDing init(availableFunds:)
Just like every TDD cycle, you first need to write a failing test. Add the following
below the previous test, which should generate a compiler error:

func testInitAvailableFunds_setsAvailableFunds() {
// given
let availableFunds = Decimal(100)

// when
let sut = CashRegister(availableFunds: availableFunds)

// then
XCTAssertEqual(sut.availableFunds, availableFunds)
}

This test is more complex than the first, so you’ve broken it into three parts: given,
when and then. It’s useful to think of unit tests in this fashion:

• Given a certain condition…

• When a certain action happens…

• Then an expected result occurs.

In this case, you’re given availableFunds of Decimal(100). When you create the
sut via init(availableFunds:), then you expect sut.availableFunds to equal
availableFunds.

Note: if the given, when and then sections are very simple, you might choose
to omit these comment lines. We’ve included them throughout this chapter for
clarity, but in your own projects, use your own judgement to decide whether
having or omitting them makes the code easier to read.

What’s the name sut about? sut stands for system under test. It’s a very common
name used in TDD that represents whatever you’re testing. This name is used
throughout this book for this very purpose.

This code doesn’t compile yet because you haven’t defined


init(availableFunds:). Compilation failures are treated as test failures, so you’ve
completed the red step.

raywenderlich.com 31
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

You next need to get this to pass. Add the following code inside CashRegister:

var availableFunds: Decimal

init(availableFunds: Decimal = 0) {
self.availableFunds = availableFunds
}

CashRegister can now be initialized with availableFunds.

Press Play to execute all of the tests, and you should see output similar to the
following in the console:

Test Suite 'CashRegisterTests' started


at 2021-07-22 17:03:58.245
Test Case '-[__lldb_expr_5.CashRegisterTests
testInit_createsCashRegister]' started.
Test Case '-[__lldb_expr_5.CashRegisterTests
testInit_createsCashRegister]' passed (0.081 seconds).
Test Case '-[__lldb_expr_5.CashRegisterTests
testInitAvailableFunds_setsAvailableFunds]' started.
Test Case '-[__lldb_expr_5.CashRegisterTests
testInitAvailableFunds_setsAvailableFunds]' passed
(0.003 seconds).
Test Suite 'CashRegisterTests' passed at
2021-07-22 17:03:58.331.
Executed 2 tests, with 0 failures (0 unexpected) in 0.085
(0.086) seconds

Both tests pass, so you’ve completed the green step.

You next need to refactor both your app and test code. First, take a look at the test
code.

testInit_createsCashRegister is now obsolete: There isn’t an init() method


anymore. Rather, this test is actually calling init(availableFunds:) using the
default parameter value of 0 for availableFunds.

Delete testInit_createsCashRegister entirely.

What about the app code? Does it make sense to have a default parameter value of 0
for availableFunds? This was useful to get both testInit and
testInitAvailableFunds to compile, but should this class actually have this?

raywenderlich.com 32
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Ultimately, this is a design decision:

• If you choose to keep the default parameter, you might consider adding a test for
testInit_setsDefaultAvailableFunds, in which you’d verify availableFunds
is set to the expected default value.

• Alternatively, you might choose to remove the default parameter, if you decide it
doesn’t make sense to have this.

For this example, assume that it doesn’t make sense to have a default parameter. So,
delete the default parameter value of 0. Your initializer should then look like this:

init(availableFunds: Decimal) {

Press Play to execute your remaining test, and verify it still passes.

The fact that testInitAvailableFunds passes after refactoring


init(availableFunds:) gives you a sense of security that your changes didn’t
break existing functionality. This added confidence in refactoring is a major benefit
of TDD!

You’ve now completed the refactor step, and you’re ready to move onto the next
TDD Cycle.

TDDing addItem
You’ll next TDD addItem to add an item’s cost to a transaction. As always, you first
need to write a failing test. Add the following below the previous test, which should
generate compiler errors:

func testAddItem_oneItem_addsCostToTransactionTotal() {
// given
let availableFunds = Decimal(100)
let sut = CashRegister(availableFunds: availableFunds)

let itemCost = Decimal(42)

// when
sut.addItem(itemCost)

// then
XCTAssertEqual(sut.transactionTotal, itemCost)
}

raywenderlich.com 33
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

This test doesn’t compile because you haven’t defined addItem(_:) or


transactionTotal yet.

To fix this, add the following property right after availableFunds within
CashRegister:

var transactionTotal: Decimal = 0

Finally, add addItem(_:) below init(availableFunds:):

func addItem(_ cost: Decimal) {


transactionTotal = cost
}

Here, you set transactionTotal to the passed-in cost. But that’s not exactly right,
or is it?

Remember how you’re supposed to write the bare minimum code to get a test to
pass? In this case, the bare minimum code required to add a single transaction is
setting transactionTotal to the passed-in cost of the item, not adding it! Thereby,
this is what you did.

Press Play, and you should see console output indicating all tests have passed. This
is technically correct, for one item. Just because you’ve completed a single TDD Cycle
doesn’t mean that you’re done. Rather, you must implement all of your app’s
features before you’re done!

In this case, the missing “feature” is the ability to add multiple items to a transaction.
Before you do this, you need to finish the current TDD cycle by refactoring what
you’ve written.

Start by looking over your test code. Is there any duplication? There sure is! Check
out these lines:

let availableFunds = Decimal(100)


let sut = CashRegister(availableFunds: availableFunds)

This code is common to both testInitAvailableFunds and testAddItem. To


eliminate this duplication, you’ll create instance variables within
CashRegisterTests.

Add the following right after the opening curly brace for CashRegisterTests:

var availableFunds: Decimal!


var sut: CashRegister!

raywenderlich.com 34
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Just like production code, you’re free to define whatever properties, methods and
classes you need to refactor your test code. There’s even a pair of special methods to
“set up” and “tear down” your tests, conveniently named setUp() and tearDown().

setUp() is called right before each test method is run, and tearDown() is called
right after each test method finishes.

These methods are the perfect place to move the duplicated logic. Add the following
below your test properties:

// 1
override func setUp() {
super.setUp()
availableFunds = 100
sut = CashRegister(availableFunds: availableFunds)
}

// 2
override func tearDown() {
availableFunds = nil
sut = nil
super.tearDown()
}

Here’s what this does:

1. Within setUp(), you first call super.setUp() to give the superclass a chance to
do its setup. You then set availableFunds and sut.

2. Within tearDown(), you do the opposite. You set availableFunds and sut to
nil, and lastly call super.tearDown().

You should always nil any properties within tearDown() that you set within
setUp(). This is due to the way the XCTest framework works: It instantiates each
XCTestCase subclass within your test target, and it doesn’t release them until all of
the test cases have run. Thereby, if you have a many test cases, and you don’t set
their properties to nil within tearDown, you’ll hold onto the properties’ memory
longer than you need. Given enough test cases, this can even cause memory and
performance issues when running your tests.

raywenderlich.com 35
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Note: there’s also alternative setup/teardown methods: setUpWithError()


throws and tearDownWithError() throws. Use these methods instead if your
code has the possibility of throwing an error during setup and/or teardown.

You can now use these instance properties to get rid of the duplicated logic in the
test methods. Replace the contents of testInitAvailableFunds with this single
line:

XCTAssertEqual(sut.availableFunds, availableFunds)

This makes it very easy to read, and this removes the need for the given and when
comments.

Next, replace the contents of testAddItem with the following:

// given
let itemCost = Decimal(42)

// when
sut.addItem(itemCost)

// then
XCTAssertEqual(sut.transactionTotal, itemCost)

Ah, that’s much simpler too! By moving the initialization code into setup(), you can
clearly see this method is simply exercising addItem(_:). Press Play to confirm all
tests still pass.

This completes the refactoring work, so you’re now ready to move onto the next TDD
Cycle.

raywenderlich.com 36
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Adding two items


testAddItem_oneItem confirms addItem() passes for one item, but it won’t pass for
two… or will it? A new test can definitively prove this.

Add the following test below


testAddItem_oneItem_addsCostToTransactionTotal:

func testAddItem_twoItems_addsCostsToTransactionTotal() {
// given
let itemCost = Decimal(42)
let itemCost2 = Decimal(20)
let expectedTotal = itemCost + itemCost2

// when
sut.addItem(itemCost)
sut.addItem(itemCost2)

// then
XCTAssertEqual(sut.transactionTotal, expectedTotal)
}

This test calls addItem() twice, and validates whether the transactionTotal
accumulates.

Press Play, and you’ll see the console output indicates the test failed:

Test Case '-[__lldb_expr_14.CashRegisterTests


testAddItem_twoItems_addsCostsToTransactionTotal]' started.
CashRegister.playground:89: error:
-[__lldb_expr_14.CashRegisterTests
testAddItem_twoItems_addsCostsToTransactionTotal] :
XCTAssertEqual failed: ("20") is not equal to ("62") -
Test Case '-[__lldb_expr_14.CashRegisterTests
testAddItem_twoItems_addsCostsToTransactionTotal]'
failed (0.008 seconds).
...
Test Suite 'CashRegisterTests' failed at
2021-07-22 17:23:52.413
Executed 3 tests, with 1 failure (0 unexpected) in 0.141
(0.142) seconds

You next need to get this test to pass. To do so, replace the contents of addItem(_:)
with this:

transactionTotal += cost

raywenderlich.com 37
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Here, you’ve replaced the = operator with += to add to the transactionTotal


instead of set it. Press the Play button again, and you’ll now see that all tests pass.

You lastly need to refactor your code. Notice any duplication? How about the
itemCost variable used in both addItem tests? Yep, you should pull this into an
instance property.

Add the following below the instance property for availableFunds within
CashRegisterTests:

var itemCost: Decimal!

Next, add this line right after setting availableFunds within setUp():

itemCost = 42

Since you set this property within setUp(), you also must nil it within tearDown().
Add the following right after setting availableFunds to nil within tearDown():

itemCost = nil

Next, delete these two lines from testAddItem_oneItem:

// given
let itemCost = Decimal(42)

Likewise, delete this one line from testAddItem_twoItems:

let itemCost = Decimal(42)

When you’re done, the only itemCost to remain should be the instance property
defined on CashRegisterTests. Then, press the Play button to verify that all tests
continue to pass.

See any other duplication within CashRegisterTests? What about this line?

sut.addItem(itemCost)

This is common to both testAddItem_oneItem and testAddItem_twoItems. Should


you try to eliminate this duplication?

Remember how setUp() is called before every test method is run? You already have
one test method that doesn’t require this call, testInitAvailableFunds.

raywenderlich.com 38
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

As you continue to TDD CashRegister, you’ll likely write other methods that won’t
need to call addItem(_:). Consequently, you shouldn’t move this call into setUp().

When to refactor code to eliminate duplication is more an art than an exact science.
Do what you think is best while you’re going along, but don’t be afraid to change
your decision later if needed!

Challenge
CashRegister is off to a great start! However, there’s still more work to do.
Specifically, you need a method to accept payment. To keep it simple, you’ll only
accept cash payments — no credit cards or IOUs allowed!

Your challenge is to TDD this new method, acceptCashPayment(_ cash:).

Try to solve this yourself first without help. If you get stuck, see below for hints.

For this challenge, you need to create two test methods within CashRegisterTests.

First, create a test method called


testAcceptCashPayment_subtractsPaymentFromTransactionTotal. Within this,
do the following:

• Call sut.addItem(_:) to set up a “transaction in progress.”

• Call sut.acceptCashPayment(_:) to accept payment.

• Assert transactionTotal has the payment subtracted from it.

Next, implement acceptCashPayment(_:) within CashRegister to make the test


pass, and refactor as needed.

Create a second test method called


testAcceptCashPayment_addsPaymentToAvailableFunds. Therein, do the
following:

• Call sut.addItem(_:) to set up a current transaction.

• Call sut.acceptCashPayment(_:) to accept payment.

• Assert the availableFunds has the payment added to it.

Finally, update acceptCashPayment(_:) to make this test pass, and refactor as


needed.

raywenderlich.com 39
iOS Test-Driven Development by Tutorials Chapter 2: The TDD Cycle

Key points
You learned about the TDD Cycle in this chapter. This has four steps:

1. Red: Write a failing test.

2. Green: Make the test pass.

3. Refactor: Clean up both your app and test code.

4. Repeat: Do it again until all of your features are implemented.

Xcode playgrounds are a great way to learn new concepts, just like you learned the
TDD Cycle in this chapter. In real-world development, however, you typically create
unit test targets within your iOS projects, instead of using playgrounds. Fortunately,
TDD works even better with apps than playgrounds!

Continue onto the next section to learn about using TDD in iOS apps.

raywenderlich.com 40
Section II: Beginning TDD

This section will teach you the basics of test-driven development (TDD). You’ll learn
about setting up your app for TDD, test expressions, dependency injection, mocks
and test expectations.

Along the way, you’ll build a fitness app to learn the basics of TDD through hands-on
practice.

raywenderlich.com 41
3 Chapter 3: TDD App Setup
By Michael Katz

By now, you should either be sold on Test-Driven Development (TDD) or at least


curious. Following the TDD methodology helps you write clean, concise and correct
code. This chapter will guide you through its fundamentals and give you a feel for
how Xcode testing works by creating a test target with a few tests. You’ll do this
while learning the key concepts of TDD.

By the end of the chapter, you’ll be able to:

• Create a test target and run unit tests.

• Write unit tests that verify data and state.

About the FitNess app


In this chapter, you’ll build a fun step-tracking app: FitNess. FitNess is the premier
fitness-coaching app based on the Loch Ness workout. Users have to outrun,
outswim or outclimb Nessie, the fitness monster. The app aims to motivate user
movement by having them outpace Nessie. If they fail, Nessie eats their avatar.

Begin with the starter project for Chapter 3. This is a shell app. It comes with some
things already wired to save you some busy work. It’s mostly bare-bones since the
goal is to lead development with writing tests.

Build and run. You’ll see the app doesn’t do anything.

raywenderlich.com 42
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Your first test


First things first: You can’t run any tests without a test target. A test target is a
binary that contains the test code and executes during the test phase.

While it’s built alongside the app, it’s not included in the app bundle, which means
your test code can contain code that doesn’t ship to your users. However, just
because your users won’t see this code doesn’t mean you can write lower-quality
code.

The TDD philosophy treats tests as first-class code, meaning they should fit the same
standards as your production code in terms of readability, naming, error handling
and coding conventions.

Adding a test target


First, create a test target. Select FitNess in the Project navigator to show the project
editor. Click the + button at the bottom of the targets list to add a new target.

raywenderlich.com 43
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Scroll down to the Test section and select Unit Testing Bundle. Click Next.

Did you notice the other bundle — UI Testing Bundle? UI testing uses automation
scripting to verify views and app state. This type of testing isn’t necessary for
adherence to TDD methodology and is outside the scope of this book.

On the next screen, double-check the Product Name is FitNessTests, the


Language is Swift and the Target to be Tested is FitNess. Then click Finish.

Voila! You now have a FitNessTests target. Xcode also added a FitNessTests group
in the Project navigator with FitNessTest.swift and an Info.plist for the target.

raywenderlich.com 44
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Deciding what to test


The unit test target template comes with a unit test class: FitNessTests. Ironically, it
doesn’t actually test anything. Delete FitNessTests.swift.

Right now, the app does nothing since there’s no business logic. There’s only one
button, and users expect that tapping Start will start the activity. Therefore, you
should start with… Start.

The TDD process requires writing a test first, which means determining the smallest
unit of functionality. This unit is where to start: The smallest thing that does
something.

The App Model directory contains an AppState enum, which, not surprisingly,
represents the app’s different potential states. The AppModel class contains the app’s
current state.

The minimum functionality to start the app is having the Start button put the app
into a started, or in-progress, state. Two statements support this goal:

1. The app should start in .notStarted to let the UI render the welcome
messaging.

2. When the user taps Start, the app should move into .inProgress to track user
activity and display updates.

The statements are actually assertions and what you’ll use to define test cases.

Adding a test class


Right-click FitNessTests in the project navigator. Select New File. In the iOS tab,
select Unit Test Case Class and click Next.

raywenderlich.com 45
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Name the class AppModelTests. A good naming convention takes the name of the
file or class you’re testing and appends the suffix: Tests. In this case, you’re writing
tests for AppModel. Click Next.

Make sure the group is FitNessTests and only the FitNessTests target is checked.
Click Create. If Xcode asks to create an Objective-C bridging header, click Don’t
Create. There’s no Objective-C in this project.

You now have a fresh test class to start adding test cases. Delete the template
methods testExample() and testPerformanceExample(). Ignore
setUpWithError() and tearDownWithError() for now.

raywenderlich.com 46
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Red-Green-Refactor
The name of the game in TDD is red, green, refactor. To iteratively writing tests in
this fashion, you:

1. Write a test that fails (red).

2. Write the minimum amount of code to make the test pass (green).

3. Clean up test(s) and code as needed (refactor).

4. Repeat the process until you cover all the logic cases.

Writing a red test


Add your first failing-to-compile test to the class:

func testAppModel_whenInitialized_isInNotStartedState() {
let sut = AppModel()
let initialState = sut.appState
XCTAssertEqual(initialState, AppState.notStarted)
}

This method creates an app model and gets its appState. The third line of the test
performs the assertion that the state matches the expected value— more on that in a
little bit.

Next, run the test.

Xcode provides several ways to run a test:

• Click the diamond next to an individual test in the line number bar. This method
runs just that test.

raywenderlich.com 47
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

• Click the diamond next to the class definition to run all the tests in the file.

• Click Play to the right of a test or test class in the Test navigator. This process
runs an individual test, a whole test file or all the tests in a test target.

• Use the Product ▸ Test menu action, or Command + U, to run all the tests in the
scheme. Right now, there’s one test target, so it would only run all the tests in
FitNessTests.

• Press Control + Option + Command + U. This method runs the test function if
the editor cursor is within a test function or the whole test file if the cursor is in a
test file but outside a specific test function.

That’s a lot of ways to run a test! Choose whichever one you prefer to run your one
test.

Before the test executes, you’ll receive two compilation errors, which means this is a
failing test! Congratulations! A failing test is the first step of TDD! Remember that
red isn’t just good, but necessary at this stage. If the test were to pass without any
code written, then it’s not a worthwhile test.

Making the test green


The first issue with this test is the test code doesn’t know what the heck an
AppModel is. Add this statement to the top of the file:

import FitNess

In Xcode, although application targets aren’t frameworks, they are modules, and test
targets can import them as if they were a framework. Like frameworks, you have to
import them in each Swift file, so the compiler is aware of what the app contains.

If the compile error cannot find 'AppModel' in scope doesn’t resolve itself, you
can make Xcode rebuild the test target via the Product ▸ Build For ▸ Testing menu
or the default keyboard shortcut Shift + Command + U.

You’re not done fixing compiler errors yet. Now, the compiler will complain about
Value of type 'AppModel' has no member 'appState'.

Open AppModel.swift and add the following variable to the class directly above
init():

public var appState: AppState = .notStarted

Run the test again. You’ll get a green checkmark next to the test since it passes.

raywenderlich.com 48
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Notice how the only app code you wrote was to make that one pass.

Congrats, you now have a green test! This test is trivial: You’re testing the default
state of an enum variable as the result of an initializer. That means, in this case,
there’s nothing to refactor. You’re done!

Writing a more interesting test


The previous test asserted the app starts in a not started state. Next, assert the app
can go from not started to in-progress.

Add the following test to the end of your class before the closing bracket:

func testAppModel_whenStarted_isInInProgressState() {
// 1 given app in not started
let sut = AppModel()

// 2 when started
sut.start()

// 3 then it is in inProgress
let observedState = sut.appState
XCTAssertEqual(observedState, .inProgress)
}

You can break this test into three parts:

1. The first line creates an AppModel. The previous test ensures the model
initializes to .notStarted.

2. The second line calls a yet-to-be-created start method.

3. The last two lines verify the state should then be equal to .inProgress.

Run the tests. Once again, you have a red test that doesn’t compile. Next, you’ll fix
the compiler errors.

Open AppModel.swift and add the following method below init():

public func start() {


}

raywenderlich.com 49
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Now, the app should compile. Run the tests.

The test fails! Obviously, it fails because start() has no code. Add the minimum
code to start(), so the test passes:

appState = .inProgress

Run the tests again. The test passes!

Note: It’s straightforward logic that an empty start() fails the test. TDD is
about discipline, and it’s good practice to strictly follow the process while
learning. With more experience, it’s OK to skip the literal build and test step
after getting the test to compile. However, you can’t skip writing the minimum
amount of code so the test passes. It’s essential to the TDD process and is
what ensures adequate coverage.

raywenderlich.com 50
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Test nomenclature
These tests follow some TDD nomenclature and naming best practices. Take another
look at the second test, line-by-line:

1. func testAppModel_whenStarted_isInInProgressState() {
The test function name should describe the test. The test name shows up in the test
navigator and test logs. With a large test suite that runs in a continuous integration
rig, you can just look at the test failures and see the problem. Avoid creating tests
with non-descript names like test1 and test2.

The naming scheme used here has up to four parts:

i. All tests must begin with test.

ii. AppModel This says an AppModel is the system under test (sut).

iii. whenStarted is the condition or state change that is the catalyst for the test.

iv. isInInProgressState is the assertion about what the sut’s state should be after
the when happens.

This naming convention also helps keep the test code focused on a specific
condition. Any code that doesn’t flow naturally from the test name belongs in
another test.

2. let sut = AppModel()


Here, you make the system under test explicit by naming it sut. This test is in the
AppModelTests test case subclass and is a test on AppModel. It may be slightly
redundant, but it’s nice and explicit.

3. sut.start()
This is the behavior to test. In this case, the test covers what happens when you call
start().

4. let observedState = sut.appState


Here, you define a property that holds the value you observed while executing
the app code.

5. XCTAssertEqual(observedSate, .inProgress)

raywenderlich.com 51
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

The last part is the assertion about what happens to sut when it starts. The stated
logical assertions correspond directly in XCTest to XCTAssert functions.

You can divide a test method into given/when/then:

• The first part of a test is the things that are given. That’s the initial state of the
system.

• The second part is the when, which is the action, event or state change that acts
on the system.

• The third part, or then, is testing the expected state after the when.

TDD is a process, not a naming convention. This book uses the convention outlined
here, but you can still follow TDD on your own using whatever naming conventions
you’d like. What’s important is your write failing tests, add the code that makes the
test pass, then refactor and repeat until the application is complete.

Structure of XCTestCase subclass


XCTest is in the family of test frameworks derived from XUnit. Like so many good
object-oriented things, XUnit comes from Smalltalk, where it was SUnit. It’s an
architecture for running unit tests. The X is a stand-in for the programming
language. For example, in Java, it’s JUnit, and in Objective-C, it’s OCUnit. In Swift,
it’s just XCTest.

With XUnit, tests are methods whose names start with test and are part of a test
case class. You can group test cases into a test suite. Test runner is a program that
knows how to find test cases in the suite, run them and gather and display results.
It’s Xcode’s test runner that executes when you run the test phase of a scheme.

Each test case class has a setUpWithError() and tearDownWithError() used to set
up global and class state before and after each test method runs. Unlike other XUnit
implementations, XCTest doesn’t have lifecycle methods that run only once for a
whole test class or the test target.

raywenderlich.com 52
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

These methods are essential because of a few subtle but extremely important
gotchas:

• You manage XCTestCase subclass lifecycles outside the test execution, and any
class-level state persists between test methods.

• Because it isn’t explicitly defined, you can’t rely on the order in which test classes
and test methods run.

Therefore, it’s important to use setUpWithError() and tearDownWithError() to


clean up and make sure state is in a known position before each test.

Setting up a test
Both tests need an AppModel() to test. It’s common for test cases to use a common
sut object.

In AppModelTests.swift, add the following variable to the top of the class:

var sut: AppModel!

This variable sets aside storage for an AppModel to use in the tests. It’s force-
unwrapped in this case because you don’t have access to the class initializer. Instead,
you have to set up variables later, for example, in setUpWithError().

Next, update setUpWithError() to:

override func setUpWithError() throws {


try super.setUpWithError()
sut = AppModel()
}

Finally, in both testAppModel_whenInitialized_isInNotStartedState() and


testAppModel_whenStarted_isInInProgressState(), remove:

let sut = AppModel()

Build and test. The tests both still pass.

The second test modifies the appState of sut. Without the setup code, the test order
could matter because the first test asserts the initial state of sut. But for now, the
order doesn’t matter since sut is re-instantiated for each test.

raywenderlich.com 53
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Tearing down a test


A related gotcha with XCTestCases is it won’t be deinitialized until all the tests are
complete. That means it’s essential to clean up a test’s state after runs to control
memory usage, clean up the file system or otherwise put things back the way you
found them.

Update tearDownWithError():

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}

So far, it’s a pretty simple test case, and the only persistent state is in sut, so clearing
it in tearDown is good practice. It helps ensure that any new global behavior you add
in the future won’t affect previous tests.

Your next set of tests


You added a little bit of app logic. But there isn’t any user-visible functionality yet.
You need to wire Start to change the app state and reflect it to the user.

Hold up! This is test-driven development, and that means writing the test first.

Since StepCountController contains the logic for the main screen, create a new
Unit Test Case Class named StepCountControllerTests in the FitNessTests
target.

raywenderlich.com 54
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Test target organization


Take a moment to think about the test target organization. As you continue to add
test cases when building the app, they’ll become hard to find and maintain in one
unorganized list. Remember, unit tests are first-class code and should have the same
level of scrutiny as app code. That means keeping them organized.

In this book, you’ll use the following organization:

Test Target
⌊ Cases
⌊ Group 1
⌊ Tests 1
⌊ Tests 2
⌊ Group 2
⌊ Tests
⌊ Mocks
⌊ Helper Classes
⌊ Helper Extensions

• Cases: The group for the test cases. These are organized in a parallel structure to
the app code, making it easy to navigate between the app class and its tests.

• Mocks: For code that stands in for functional code, letting you separate
functionality from implementation. For example, developers commonly mock
network requests. You’ll build these in later chapters.

• Helper classes and extensions: For additional code you write to make the test
code easier to write, but which doesn’t directly test or mock functionality.

Put the two classes already in the target in a group named Cases.

Next, put AppModelTests.swift in a App Model group. Then put


StepCountControllerTests.swift in a UI Layer group.

When it’s all done, your target structure will look like this:

As you add new tests, keep them organized in groups.

raywenderlich.com 55
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Using @testable import


Open StepCountControllerTests.swift.

Delete the testExample() and testPerformanceExample() stubs. Then delete the


comments in setUpWithError() and tearDownWithError().

Next, add the following class variable above setUpWithError():

var sut: StepCountController!

Build the test class now. You’ll see an error, cannot find type
'StepCountController' in scope, because the class is specified as internal since
it doesn’t explicitly define access control.

There are two ways to fix this error. The first is to declare StepCountController as
public, making that class available outside the FitNess module and usable by the
test class. However, this would violate SOLID principles by making the view visible
outside of the app.

Fortunately, Xcode provides a way to expose data types for testing without making
them available for general use through the @testable attribute.

Add the following to the top of the file, under import XCTest:

@testable import FitNess

This code makes symbols that are open, public, and internal available to the test
case. Note that this attribute is only available in test targets and will not work in app
or framework code. Now, the test can successfully build.

Next, update setUpWithError() and tearDownWithError() as follows:

override func setUpWithError() throws {


try super.setUpWithError()
sut = StepCountController()
}

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}

raywenderlich.com 56
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Testing a state change


Now here comes the fun part! There are two things to check when the user taps
Start: Does the app state update and does the UI update. Take each one in turn.

Add the following test method below tearDownWithError():

func testController_whenStartTapped_appIsInProgress() {
// when
sut.startStopPause(nil)

// then
let state = AppModel.instance.appState
XCTAssertEqual(state, AppState.inProgress)
}

This method tests if the app state is inProgress when you call startStopPause().

Build and test. You’ll get a test failure because you haven’t implemented
startStopPause yet. Remember, test failures at this point are good!

Open StepCountController.swift, and add the following code to


startStopPause():

AppModel.instance.start()

Build and test again. Now the test passes!

Testing UI updates
UI testing with UI Automation is a different type of testing and is outside the scope
of this book. However, there are plenty of UI aspects that can, and should, be unit
tested. SwiftUI’s declarative nature means a view’s internal structure is deliberately
difficult to access. UI Automation makes that task significantly easier, but you can
also write valuable unit tests by separating the state-controlling logic from the view
hierarchy.

raywenderlich.com 57
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Writing the test


Add the following test case at the bottom of StepCountControllerTests:

func testController_whenStartTapped_buttonLabelIsPause() {
// when
sut.startStopPause(nil)

// then
let text = sut.startButton.title(for: .normal)
XCTAssertEqual(text, AppState.inProgress.nextStateButtonLabel)
}

Like the previous tests, this test performs startStopPause(). However, this time the
test checks if the button text updates.

You may have noticed that this test is almost the same as the previous one. It has the
same initial conditions and “when” action. The critical difference is that this tests a
different state change.

TDD best practice is to have one assert per test. With well-named test methods,
when the test fails, you’ll know exactly where the issue is because there’s no
ambiguity between multiple conditions. You’ll tackle cleaning up this kind of
redundancy in later chapters.

Another good practice illustrated here is using


AppState.inProgress.nextStateButtonLabel instead of hard-coding the string.
By using the app’s value, the assert is testing behavior and not a specific value. If the
string changes or gets localized, the test won’t have to change to accommodate that.

Since this is TDD, the test will fail if you run it. Fix the test by adding the appropriate
code to the end of startStopPause(_:):

let title = AppModel.instance.appState.nextStateButtonLabel


startButton.setTitle(title, for: .normal)

Now, build and test again for a green test. You can also build and run to try out the
functionality.

Tapping the Start button turns it into a Pause button.

raywenderlich.com 58
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Next, add the following method below startStopPause():

Part of writing comprehensive unit tests is to make implicit assumptions into


explicit assertions. Insert the following code between tearDownWithError() and
testController_whenStartTapped_appIsInProgress():

// MARK: - Initial State

func testController_whenCreated_buttonLabelIsStart() {
let text = sut.startButton.title(for: .normal)
XCTAssertEqual(text, AppState.notStarted.nextStateButtonLabel)
}

// MARK: - In Progress

This test checks the button’s label after it’s created to make sure it reflects
the .notStarted state.

This also adds some MARKs to the file to help divide the test case up into sections. As
the classes get more complicated, the test files will grow quite large, so it’s important
to keep them well organized.

Build and test. Hurray, another failure! Go ahead and fix the test.

The last two tests rely on certain initial conditions for their states. For example, in
testView_whenStartTapped_buttonLabelIsPause, the desire is to test for the
transition from .notStarted to .inProgress. But the test could also pass if the
view started in .inProgress.

Part of writing comprehensive unit tests is to make implicit assumptions into


explicit assertions. Insert the following code between tearDownWithError() and
testController_whenStartTapped_appIsInProgress():

let title = AppState.notStarted.nextStateButtonLabel


startButton.setTitle(title, for: .normal)

The test is not quite ready yet to pass. Go back to the tests, and add at the top of
testController_whenCreated_buttonLabelIsStart() the following lines:

// given
sut.viewDidLoad()

Now, build and test and the tests will pass. The call to viewDidLoad() is needed
because the sut is not actually loaded from the xib and put into a view hierarchy, so
the view lifecycle methods do not get called. You’ll see in Chapter 4, “Test
Expressions,” how to get a properly loaded view controller for testing.

raywenderlich.com 59
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Refactoring
If you look at StepCountController.swift, the code that sets the button text is
awfully redundant. When building an app using TDD, after you get all the tests to
pass, you can then refactor the code to make it more efficient, readable,
maintainable, etc. You can feel free to modify the both the app code and test code at
will, resting easy because you have a complete set of tests to catch any issues if you
break it.

Add the following method to the bottom of StepCountController:

private func updateButton() {


let title = AppModel.instance.appState.nextStateButtonLabel
startButton.setTitle(title, for: .normal)
}

This helper method will be used in multiple places in the file — whenever the button
needs to reflect a change in app state. This can be private as this is an internal
implementation detail of the class. The behavioral methods remain internal and
can still be tested.

In viewDidLoad() and startStopPause(_:) replace the two lines that update the
title with a call to updateButton().

Build and test. The tests will all still pass. Code was changed, but behavior was kept
constant. Hooray refactoring! This type of refactoring is called Extract Method.
There is a menu item to do it available in the Editor ▸ Refactor menu in Xcode.

You’re still a long way from a complete app with a full test suite, but you are on your
way.

Challenge
There are a few things left to do with the two test classes you already made. For
example, AppModel is public when it should be internal. Update its access modifier
and use @testable import in AppModelTests.

And in StepCountControllerTests.swift there is a redundancy in the call to


startStopPause(_:). Extract that out into a helper when method.

raywenderlich.com 60
iOS Test-Driven Development by Tutorials Chapter 3: TDD App Setup

Key points
• TDD is about writing tests before writing app logic.

• Use logical statements to drive what you test.

• Each test should fail upon its first execution. Not compiling counts as a failure.

• Use tests to guide refactoring code for readability and performance.

• Good naming conventions make it easier to navigate and find issues.

Where to go from here?


Test-driven development is pretty simple in its fundamentals: Only write app code
for a unit test to pass. For the rest of the book, you’ll follow the red-green-refactor
model over and over. You’ll explore more interesting types of tests and learn how to
test things that aren’t obviously unit testable.

For more information specific to how Xcode works with tests and test targets, see the
developer documentation (https://developer.apple.com/library/archive/
documentation/DeveloperTools/Conceptual/testing_with_xcode/chapters/04-
writing_tests.html). For a jam-packed overview on iOS testing, try out this free iOS
Unit Testing and UI Testing tutorial (https://www.raywenderlich.com/21020457-ios-
unit-testing-and-ui-testing-tutorial).

In the next chapter, you’ll learn more about XCTAssert functions, testing view
controllers, code coverage and debugging unit tests.

raywenderlich.com 61
4 Chapter 4: Test
Expressions
By Michael Katz

The TDD process is straightforward, but writing good tests may not always be.
Fortunately, each year, Xcode and Swift have become more capable. This means you
have many features at your disposal that help with both writing and running tests.

This chapter covers how to use the XCTAssert functions. These are the primary
actors of the test infrastructure. You’ll go through gathering code coverage to verify
the minimum amount of testing. Finally, you’ll use the test debugger to find and fix
test errors.

In this chapter, you’ll learn about:

• XCTAssert functions

• UIViewController testing

• Code Coverage

• Test debugging

Note: Be sure to use the Chapter 4 starter project rather than continuing with
the Chapter 3 final project. It has a few new things added to it, including
placeholders for the code to add in this tutorial.

raywenderlich.com 62
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Assert methods
In Chapter 3, “TDD App Setup,” you used XCTAssertEqual exclusively. There are
several other assert functions in XCTest:

• Equality: XCTAssertEqual, XCTAssertNotEqual

• Truthiness: XCTAssertTrue, XCTAssertFalse

• Nullability: XCTAssertNil, XCTAssertNotNil

• Comparison: XCTAssertLessThan, XCTAssertGreaterThan,


XCTAssertLessThanOrEqual, XCTAssertGreaterThanOrEqual

• Erroring: XCTAssertThrowsError, XCTAssertNoThrow

Ultimately, any test case can be boiled down to a conditional: (does it meet an
expectation or not) so any test assert can be re-composed into a XCTAssertTrue.

Note: With XCTest, a test is marked as passed as long as there are no failures.
This means that it does not require a positive XCTAssert assertion. A test with
no asserts will be marked as success, even though it does not test anything!

App state
In the previous chapter, you built out the functionality to move the app from a not
started state to an in-progress one. Now is a good time to think about about the
whole app lifecycle.

Here are the possible app states, as represented by the AppState enum:

• notStarted: The initial state of the app.

• inProgress: The app is actively monitoring the activity of the user and Nessie.

• paused: The app was paused by the user. Nessie is put to sleep and the activity
tracking stops.

• completed: The user has reached their activity goal before Nessie caught up.

• caught: Nessie caught up to the user and “ate” them.

raywenderlich.com 63
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

The following diagram shows the possible state transitions:

The solid lines represent user action on the UI, and the dotted lines happen
automatically due to time or activity events. The user-based transitions will be
covered in this chapter project, and the automatic transitions will be covered in
Chapter 5: “Test Expectations.”

Asserting true and false


To build out the state transitions, you need to add some more information to the app
about the user. The completed and caught states depend on the user activity, the set
goal and Nessie’s activity. To keep the architecture clean, the app state information
will be kept separate from the raw data that is tracking the user.

Add a new unit test case class to the test target, in the Data Model group. Name it
DataModelTests. Once again, and like always, remove testExample() and
testPerformanceExample().

Next, add the following import at the top of the file:

@testable import FitNess

Next, add the following instance variable above setUpWithError():

var sut: DataModel!

Now, you have a red test case class. To fix it, open DataModel.swift and add the
following, the minimum to get the test to compile:

class DataModel {

This creates a stub class to fix the compiler error. You’ll build upon this piece-by-
piece.

raywenderlich.com 64
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Next, open DataModelTests.swift and replace setUpWithError() and


tearDownWithError() with the following:

override func setUpWithError() throws {


try super.setUpWithError()
sut = DataModel()
}

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}

These create a new DataModel for each test, and then cleans it up afterwards.

Next, add the following code to the end of DataModelTests:

// MARK: - Goal
func testModel_whenStarted_goalIsNotReached() {
XCTAssertFalse(
sut.goalReached,
"goalReached should be false when the model is created")
}

This test introduces XCTAssertFalse, which checks that the expected value is
false. Each XCTAssert function can also take an optional String message. This
message is displayed in the standard editor and report navigator’s error log when the
test fails. If you follow the test naming convention and only use one XCTAssert per
test, then you won’t normally need to supply an error message. While test name will
usually be descriptive enough to inform you why a failure occurred, it can be useful
to add a message if the assertion isn’t obvious.

Fix the non-compiling test by adding the following to DataModel in


DataModel.swift:

var goalReached: Bool { return false }

Build and test, and the test will pass.

The initial state is the boring state. Next build out the business logic.

raywenderlich.com 65
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

First, open DataModelTests.swift and add the following test method:

func testModel_whenStepsReachGoal_goalIsReached() {
// given
sut.goal = 1000

// when
sut.steps = 1000

// then
XCTAssertTrue(sut.goalReached)
}

This tests the logic “the goal is reached when the number of steps equals or exceeds
the goal.”

Now, you need a goal and steps for it to compile. Open DataModel.swift and add the
following below goalReached:

var goal: Int?


var steps: Int = 0

goal is an optional because it should be set explicitly by the user.

Now, the test will build, but fail since you previously hard coded the value of
goalReached to false.

Let’s fix that, replace goalReached with the following implementation:

var goalReached: Bool {


if let goal = goal,
steps >= goal {
return true
}
return false
}

Run the test again. It’s a little tricky on the fingers, but you can use Product ▸
Perform Action ▸ Test Again (^⌥⌘G) to re-run the last test from anywhere in
Xcode. Now, the test passes, and you’ve seen true and false asserts.

Pretty much every assert is just a Boolean test and can be rewritten as such. That
means you can write your own helper methods that look like XCTAssert’s. These just
have to eventually evaluate to a Boolean that is passed to XCTAssertTrue().

raywenderlich.com 66
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Testing Errors
If the optional goal property isn’t set, it doesn’t make sense for the app to enter the
inProgress state. Therefore starting the app without a goal is an error!

Make it a real error. Open AppModel.swift, then add the throws keyword to the
function signature of start():

func start() throws {

Now, fix the compilation errors. In StepCountController.swift replace


startStopPause(_:) with the following:

@IBAction func startStopPause(_ sender: Any?) {


do {
try AppModel.instance.start()
} catch {
showNeedGoalAlert()
}

updateUI()
}

Once you’re all done, tapping the Start button without setting a goal will display an
alert. Don’t worry about writing a test first for this right now.

Next, update testAppModel_whenStarted_isInInProgressState() in


AppModelTests.swift. Add a try? to the sut.start() line to quiet the error. This
test should still pass. You’ll come back here after changing the logic in a bit.

Next, still in AppModelTests.swift, add the following test before


testAppModel_whenStarted_isInInProgressState():

func testModelWithNoGoal_whenStarted_throwsError() {
XCTAssertThrowsError(try sut.start())
}

Using XCTAssertThrowsError, you can verify that an error is thrown if the model is
started in its initial state without a goal set.

This test fails since there is no error thrown yet. To fix that, open AppModel.swift
and add the following instance variable:

let dataModel = DataModel()

raywenderlich.com 67
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

The app model will be the container for the data model, since the app’s data is a
subset of the app’s state. The data model’s goal is needed to check for an error.

Finally, add the following guard statement at the top of start():

guard dataModel.goal != nil else {


throw AppError.goalNotSet
}

Now, build and test testModelWithNoGoal_whenStarted_throwsError, and the


test will pass.

Next, verify that setting a goal means that start() will not throw an error. Open
AppModelTests.swift and add the following under // MARK: - Given:

func givenGoalSet() {
sut.dataModel.goal = 1000
}

Next, add the following test under


testModelWithNoGoal_whenStarted_throwsError():

func testStart_withGoalSet_doesNotThrow() {
// given
givenGoalSet()

// then
XCTAssertNoThrow(try sut.start())
}

This test should go right to green, since the app logic was already written. Even
though no code had to be added or changed for this test, it’s still TDD since the tests
are leading the way. This test just completes checking all the cases of the logical
flow.

Finally, it’s time to fix all the other tests that started failing due to this change.

First, add the following to the top of


testAppModel_whenStarted_isInInProgressState:

// given
givenGoalSet()

raywenderlich.com 68
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Next, open StepCountControllerTests.swift and add the following under // MARK:


- Given:

func givenGoalSet() {
AppModel.instance.dataModel.goal = 1000
}

Finally, in the two tests under // MARK: - In Progress, add the following to the
top of each:

// given
givenGoalSet()

Build and run all the tests. They all pass! Changing these existing tests to pass again
after changing the app logic is another aspect of the refactor phase of the TDD
cycle.

If you build and run the app, there will now be an alert when Start is tapped and the
app won’t move into the inProgress state. In the next section you will update the
app with the ability to save the goal.

raywenderlich.com 69
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

View controller testing


Now that the model can have a goal set and an app state that checks it, the next
feature is to expose that the state to the user. In the previous chapter, you wrote
some unit tests for StepCountController. Now build on that with some proper view
controller unit testing.

Functional view controller testing


The important thing when testing view controllers is to not test the views and
controls directly. This is better done using UI automation tests. Here, the goal is to
check the logic and state of the view controller.

Functional testing is done by using separate methods for interacting with the UI
(callbacks, delegate methods, etc.) from logic methods (updating state).

Note: If you have experience with other app architectures, using something
like MVVM or VIPER makes it cleaner to test this type of logic. Separating a
ViewModel from the controller takes the unit-testable logic out of the
controller. For the purposes of this section, you’ll continue to build the app
using the traditional Apple MVC model. This is what’s covered in most of the
documentation and the traditional place to start developing iOS applications.

First, open StepCountControllerTests.swift. Next, add the following test under //


MARK - Goal:

func testDataModel_whenGoalUpdate_updatesToNewGoal() {
// when
sut.updateGoal(newGoal: 50)

// then
XCTAssertEqual(AppModel.instance.dataModel.goal, 50)
}

This test calls updateGoal(newGoal:) and verifies the data model has been properly
updated.

Be sure to also restore the state by adding the following line to


tearDownWithError() above super.tearDownWithError():

AppModel.instance.dataModel.goal = nil

raywenderlich.com 70
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

As expected, the test will fail. Let’s turn the test green. Open
StepCountController.swift and replace updateGoal(newGoal:) with the following:

func updateGoal(newGoal: Int) {


AppModel.instance.dataModel.goal = newGoal
}

Another beautiful green test.

Using the host app


The next requirement for the app is that the central view should show the user’s
avatar in the running position. The word should signifies an assertion, so you’ll write
one, now. First, open StepCountControllerTests.swift. Next, add the following
under // MARK: - Chase View:

func testChaseView_whenLoaded_isNotStarted() {
// when loaded, then
let chaseView = sut.chaseView
XCTAssertEqual(chaseView?.state, .notStarted)
}

The test builds, but does not pass, because chaseView is nil. What gives?

Well, there is a cheat in the code to allow the existing tests to pass. Under normal
app flow, a StepCountController is created and populated by the storyboard. It’s
already loaded by the time any app code gets to execute.

In this test the sut is initialized directly, which means its starting state is not the
same as when the app runs. Fortunately, there is a clean way to handle this.

When unit tests are run as part of the Test action in an app scheme, Xcode uses a
Host Application as specified in the target settings.

Open the General tab of the Project editor for the FitNessTests target. You’ll see
that FitNess is selected as the Host Application.

This means that running the test action, will launch the host app on the specified
destination (simulator or device). The test runner waits for the app to load before
starting the tests, and the tests are run in the app’s context.

raywenderlich.com 71
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

As a consequence, you have access to the UIApplication object and the whole View
hierarchy in the tests.

In the Project navigator, under FitNessTests target, add a new group: Test Classes.
Next, create a new Swift File, ViewControllers.swift, in that group

Next, replace the contents of this file with the following:

import UIKit
@testable import FitNess

func getRootViewController() -> RootViewController {


guard let controller =
(UIApplication.shared.connectedScenes.first as?
UIWindowScene)?
.windows
.first?
.rootViewController as? RootViewController else {
assert(false, "Did not a get RootViewController")
}
return controller
}

This function navigates the app’s window to retrieve the root view controller, which
is of type RootViewController. This helper function will be used to obtain other
view controllers.

Next, create another new group, Test Extensions under FitNessTests. In that group,
add a new Swift file: RootViewController+Tests.swift.

Replace the contents of this file with the following RootViewController extension:

import UIKit
@testable import FitNess

extension RootViewController {
var stepController: StepCountController {
return children.first { $0 is StepCountController }
as! StepCountController
}
}

Now, you have all the pieces to get the StepCountController from the host app.

raywenderlich.com 72
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Fixing the tests


Go back to StepCountControllerTests.swift, and replace setUpWithError() with
the following:

override func setUpWithError() throws {


try super.setUpWithError()
let rootController = getRootViewController()
sut = rootController.stepController
}

Remove the call to viewDidLoad from


testController_whenCreated_buttonLabelIsStart(), as this is no longer
needed.

Next, add the following method under // MARK: - Given:

func givenInProgress() {
givenGoalSet()
sut.startStopPause(nil)
}

This sets the app into the inProgressState. It’s ensured by the test
testController_whenStartTapped_appIsInProgress().

Finally, add the following test to the bottom of StepCountControllerTests:

func testChaseView_whenInProgress_viewIsInProgress() {
// given
givenInProgress()

// then
let chaseView = sut.chaseView
XCTAssertEqual(chaseView?.state, .inProgress)
}

This test will fail since the chaseView is not yet updated. Open
StepCountController.swift and replace updateChaseView() at the bottom with the
following:

private func updateChaseView() {


chaseView.state = AppModel.instance.appState
}

The test testChaseView_whenInProgress_viewIsInProgress will now pass, and no


more funny business with loading view controllers.

raywenderlich.com 73
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Note: One alternate way of retrieving and testing a view controller can be
done as follows: First, get a reference to the storyboard:
let storyboard = UIStoryboard(name: "Main", bundle: nil)

Second, get a reference to the view controller:


let stepController =
storyboard.instantiateViewcontroller(withIdentifier:
"stepController") as! StepCountController

Finally, if needed, you may load the view as follows:


stepController.loadViewIfNeeded()

Following this pattern allows you to instantiate a fresh view controller for
each test, and it affords the option to set up and tear down the view controller
for each test.

Test ordering matters


Build and test the whole target, and most of the tests should pass, but not
testController_whenCreated_buttonLabelIsStart. This test fails.

Now, only test testController_whenCreated_buttonLabelIsStart and it will


pass. Hrm… strange.

Open the report navigator and look at the result for when you last ran all the tests.

Look at the test failure: XCTAssertEqual failed: ("Optional("Pause")") is


not equal to ("Optional("Start")").

raywenderlich.com 74
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

This message tells you not only that the button text is not what’s expected, but
specifically that the button text is “Pause.” That’s what the button should say when
the app is inProgress. This violates the assumption that the test is starting with a
fresh StepCountController.

The previous change to using the host app’s StepCountController meant that a
new controller is not created every setUpWithError() and the app state is persisted.
In order to have clean tests, you need to reset the state in tearDownWithError().

To help with this, you can create a new function on AppModel to reset the state. But,
first, write the tests.

Open AppModelTests.swift. Add the following helper to the Given section:

func givenInProgress() {
givenGoalSet()
try! sut.start()
}

This puts the app in an inProgress state, allowing for the state restart test to
actually test a change.

Next, add the following to the bottom of the test case class:

// MARK: - Restart
func testAppModel_whenReset_isInNotStartedState() {
// given
givenInProgress()

// when
sut.restart()

// then
XCTAssertEqual(sut.appState, .notStarted)
}

This tests that the not-yet-added restart() puts the model back into notStarted.
To get the test to pass open AppModel.swift and add the following to AppModel:

func restart() {
appState = .notStarted
}

This function will be used as a test helper for now, but eventually will be part of the
whole app’s state cycle.

raywenderlich.com 75
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Finally, go back and fix the original issue. Change tearDownWithError() in


StepCountControllerTests.swift to:

override func tearDownWithError() throws {


AppModel.instance.dataModel.goal = nil
AppModel.instance.restart()
sut.updateUI()
try super.tearDownWithError()
}

Now, running the whole target’s tests will succeed.

Randomized order
There is also an option in the Test action of the scheme to randomize the test order.
Edit the FitNess scheme. Select the Test action. In the center pane, next to
FitNessTests is an Options… button. Click that and, in the pop-up, check
Randomize execution order. This will cause the tests to run in a random order each
time.

This can expose hidden inter-test dependencies that you wouldn’t catch with the
default ordering. The downside is that the ordering is not guaranteed, meaning you
might have missed the previous issue. Also, if an ordering issue does come up, it
might be hard to reproduce if it was very specific. Sporadic and hard-to-diagnose test
failures are one symptom that the random ordering uncovered an issue.

raywenderlich.com 76
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Code coverage
While on the subject of the scheme editor, open up the Test Action again. This time
select the Options tab. There is a checkbox for Code Coverage. Check it.

Run the tests again. After the tests succeed, open the Report navigator. Under the
latest test, there will be three reports: Build, Coverage and Log. Select Coverage to
display the coverage report.

Code coverage is the measure of how many lines of app code are executed during
tests. There will be a list of each file in the target along with the percentage of the
code lines that were executed. Having 100% or close for a file means you’re following
TDD closely. When the tests are written first, only the code needed to pass the test
gets added.

raywenderlich.com 77
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Opening up an individual file will show the coverage on a per-function or closure


basis. Double-clicking on a file or function name will open up that file in the editor.

Open StepCountController.swift and navigate to startStopPause(_:)

You’ll see a coverage annotation on the right side of the editor. The number shown
represents the number of times that line was executed. Lines with a red coloring or a
“0” indicate opportunities to add additional tests.

Lines with a striped red annotation mean that only part of that line was run.
Hovering over the stripe in the annotation bar will show you in green which part was
run and in red what was not.

In StepCountController, it looks like the startStopPause(_:) method was never


called when AppModel.start() throws an error.

The problem with testing that condition is that, when there’s an error, an alert
controller is shown. You could write a test that checks for that alert controller, but
that is really the domain of UI automation testing. You could refactor
StepCountController so that a variable is set or a callback is called in that error
case, but then you would be modifying app code just to add a test. The test would
then be testing itself and not app functionality, which does not provide any value.

The goal should be to get as close to 100% as possible. Coverage doesn’t mean the
code works, but lack of coverage means that it’s not tested. For views and view
controllers, it’s not expected to get to 100% coverage because TDD does not include
UI testing. When you combine unit tests with UI automation tests, then you should
expect to be able to cover most if not all of these files.

raywenderlich.com 78
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Debugging tests
When it comes to debugging tests, you’ve already practiced the first line of defense.
That is: “Am I testing the right thing?”

Make sure:

• You have the right assumptions in the given statements.

• Your then statements accurately reflect the desired behavior.

If nothing obvious in the test code appears, next check the test execution order for
preserved state. Also use code coverage to make sure the right code paths are taken.

After trying that, you can use some other tools in Xcode’s arsenal. To try them out,
it’s time to think about the other important actor in the app: Nessie.

Using test breakpoints


With Nessie in the picture, the data model gets a little more complicated. Here are
the new rules with Nessie:

• When Nessie’s distance is greater than or equal to the user’s, Nessie wins (the user
is caught). The user cannot be caught when the distance is at 0, which is the start
condition.

• If the user is caught by Nessie, the goal cannot be reached.

Open DataModelTests.swift and add the following test to DataModelTests:

// MARK: - Nessie
func testModel_whenStarted_userIsNotCaught() {
XCTAssertFalse(sut.caught)
}

This tests that with a fresh DataModel, the user is not caught. This test does not yet
compile.

raywenderlich.com 79
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Fix the broken test by adding the following to DataModel in DataModel.swift:

// MARK: - Nessie

let nessie = Nessie()


var distance: Double = 0

var caught: Bool {


return nessie.distance >= distance
}

This adds a Nessie to the data model, a variable to track user distance, and a
computed variable to compare the distances. A separate variable for distance is
used instead of steps to keep the calculations cleaner later on.

Even with the updated code, the test still fails. There are several ways to go about
diagnosing the problem. As you’ve already seen there are a few things to check:

• The test itself is correct, the given is a fresh DataModel as created in startUp().
The then is also correct, caught should be false.

• The DataModel code was executed, as shown by the code coverage.

A good next step is to try out the debugger. In the Breakpoint navigator, click the +
all the way at the bottom. Select Test Failure Breakpoint.

This creates a special breakpoint that halts execution when a unit test fails. Run the
test again, and the debugger will stop at the test failure.

raywenderlich.com 80
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Open the variables view, and expand self and then sut.

Here, you’ll see that both distance and steps are 0. So the app logic is doing the
right thing, Nessie is tied with the user, which should be the caught state. However,
this is a special case in which the starting condition cannot result in a capture.

To fix this, open DataModel.swift and replace caught with the following:

var caught: Bool {


return distance > 0 && nessie.distance >= distance
}

Now, the test will pass. This might have been an obvious example, but it illustrates
that you have all your normal debugging techniques available when running tests.

Completing coverage
If you take a look at the code coverage for DataModel.swift, it is no longer 100%. If
you look at the file, notice the striped annotation in the updated caught. Hovering
over the stripe shows that not of all the conditions were checked. The 0 tells you
there is more test.

Open DataModelTests.swift and add the following test cases to complete


DataModel coverage:

func testModel_whenUserAheadOfNessie_isNotCaught() {
// given
sut.distance = 1000
sut.nessie.distance = 100

// then
XCTAssertFalse(sut.caught)
}

raywenderlich.com 81
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

func testModel_whenNessieAheadofUser_isCaught() {
// given
sut.nessie.distance = 1000
sut.distance = 100

// then
XCTAssertTrue(sut.caught)
}

Now, test and check out the DataModel coverage… 100%

Finishing out the requirements


There is one final piece that hasn’t been accounted for yet: The user cannot reach
the goal if they have been caught. Add this test to the Goal tests section:

func testGoal_whenUserCaught_cannotBeReached() {
//given goal should be reached
sut.goal = 1000
sut.steps = 1000

// when caught by nessie


sut.distance = 100
sut.nessie.distance = 100

// then
XCTAssertFalse(sut.goalReached)
}

Then, to make the test pass, update goalReached in DataModel.swift:

var goalReached: Bool {


if let goal = goal,
steps >= goal, !caught {
return true
}
return false
}

Test again for success.

raywenderlich.com 82
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Challenge
In StepCountControllerTests.tearDownWithError(), there are separate calls to
reset the AppModel and the DataModel. Since the data model is a property of the app
model, refactor the data model reset into AppModel.restart(), along with the
appropriate tests.

For an extra challenge, use some of the other XCTAssert functions not yet used, like
XCTAssertNil or XCTAssertLessThanOrEqual.

A second challenge is to add the pause functionality to the app so the user can move
back and forth between .paused and .inProgress. The pause doesn’t have to do
anything else at this point, since the direct functionality will be covered in later
chapters.

Key points
• Test methods require calling a XCTAssert function.

• View controller logic can be separated in to data/state functions, which can be unit
tested and view setup and response functions, which should be tested by UI
automation.

• Test execution order matters.

• The code coverage reports can be used to make sure all branches have a minimum
level of testing.

• Test failure breakpoints are a tool on top of regular debugging tools for fixing
tests.

raywenderlich.com 83
iOS Test-Driven Development by Tutorials Chapter 4: Test Expressions

Where to go from here?


For more on code coverage, this video tutorial (https://www.raywenderlich.com/
3530-testing-in-ios/lessons/18) covers that topic. And you can learn everything and
more about debugging from the Advanced Apple Debugging & Reverse Engineering
(%5Bhttps://www.raywenderlich.com/books/advanced-apple-debugging-and-
reverse-engineering%5D) book. The tools and techniques taught in that tome are
just as applicable to test code as application code.

In the next chapter, you’ll learn about testing asynchronous functions using
XCTestExpectation.

raywenderlich.com 84
5 Chapter 5: Test
Expectations
By Michael Katz

In the previous chapters you built out the app’s state based upon what the user can
do with the Start button. The main part of the app relies on responding to changes as
the user moves around and records steps. These actions create events outside the
program’s control. XCTestExpectation is the tool for testing things that happen
outside the direct flow.

In this chapter you’ll learn:

• General test expectations

• Notification expectations

Use this chapter’s starter project instead of continuing on from the previous’ final, as
it has some additions to help you out.

Using an expectation
XCTest expectations have two parts: the expectation and a waiter. An expectation
is an object that you can later fulfill. The wait method of XCTestCase tells the
test execution to wait until the expectation is fulfilled or a specified amount of time
passes.

In the last chapter you built out the app states corresponding to direct user action:
in progress, paused, and not started. In this chapter you’ll add support for caught
and completed.

raywenderlich.com 85
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

These state transitions occur in response to asynchronous events outside the user’s
control.

The red-shaded states have already been built. You’ll be adding the grey states.

Writing an asynchronous test


In order to react to an asynchronous event, the code needs a way to listen for a
change. This is commonly done through a closure, a delegate method, or by
observing a notification.

To test caught and completed state changes that asynchronously update in


AppModel, you’ll add a callback closure. The first step is to write the test!

Open AppModelTests.swift and add the following test under // MARK: - State
Changes:

func testAppModel_whenStateChanges_executesCallback() {
// given
givenInProgress()
var observedState = AppState.notStarted

// 1
let expected = expectation(description: "callback happened")
sut.stateChangedCallback = { model in
observedState = model.appState
// 2
expected.fulfill()
}

// when
sut.pause()

// then

raywenderlich.com 86
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

// 3
wait(for: [expected], timeout: 1)
XCTAssertEqual(observedState, .paused)
}

This test updates the appState using sut.pause then checks that
stateChangedCallback gets triggered and sets observedState to the new value.
You are using a few new things in this test:

1. expectation(description:) is an XCTestCase method that creates an


XCTestExpectation object. The description helps identify a failure in the test
logs. You’ll see shortly how expected is used to track if and when the expectation
is fulfilled.

2. fulfill() is called on the expectation to indicate it has been fulfilled -


specifically, the callback has occurred. Here stateChangedCallback will trigger
on sut when a state change occurs.

3. wait(for:timeout:) causes the test runner to pause until all expectations are
fulfilled or the timeout time (in seconds) passes. The assertion will not be called
until the wait completes.

The test won’t compile, because stateChangedCallback doesn’t yet exist. Open
AppModel.swift, add the following to the class:

var stateChangedCallback: ((AppModel) -> Void)?

Adding this property allows the test to build. Now run it, and you’ll see the following
failure in the console:

Asynchronous wait failed: Exceeded timeout of 1 seconds, with


unfulfilled expectations: "callback happened".

The expectation never got fulfilled, so the test failed after the 1 second wait timeout.

To fix it, change appState in AppModel to match the following:

private(set) var appState: AppState = .notStarted {


didSet {
stateChangedCallback?(self)
}
}

The callback is now triggered each time AppState is set.

raywenderlich.com 87
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Back in AppModelTests.swift, clean up the callback reference by adding the


following to the top of tearDownWithError:

sut.stateChangedCallback = nil

Run the test again, and now it will pass!

Note: It is best practice to always call fulfill in the completion block, then
test for errors or other negative conditions using XCTAssert after the wait.
Timeout should not be used to signal a test failure, as it adds significant time
to the test.

Testing for true asynchronicity


The last test checks that the callback is called in direct response to an update on the
sut. Next, you’ll tackle a more indirect usage via updates to the view controller.
Open StepCountControllerTests.swift at the end of // MARK: - Terminal
States add the following two tests:

func testController_whenCaught_buttonLabelIsTryAgain() {
// given
givenInProgress()
let exp = expectation(description: "button title change")
let observer = ButtonObserver()
observer.observe(sut.startButton, expectation: exp)

// when
whenCaught()

// then
waitForExpectations(timeout: 1)
let text = sut.startButton.title(for: .normal)
XCTAssertEqual(text, AppState.caught.nextStateButtonLabel)
}

func testController_whenComplete_buttonLabelIsStartOver() {
// given
givenInProgress()
let exp = expectation(description: "button title change")
let observer = ButtonObserver()
observer.observe(sut.startButton, expectation: exp)

// when
whenCompleted()

raywenderlich.com 88
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

// then
waitForExpectations(timeout: 1)
let text = sut.startButton.title(for: .normal)
XCTAssertEqual(text, AppState.completed.nextStateButtonLabel)
}

These tests observe the startButton title to confirm it properly updates after model
state changes.

observe(_:expectation:) will fulfill the passed expectation (exp) when the


textLabel of sut.startButton is updated. This requires the ButtonObserver
helper class, which you’re about to create!

Add a new Swift File to the Test Classes group and name it ButtonObserver.swift.
Replace the contents of the file with the following:

import XCTest

class ButtonObserver {
var token: NSKeyValueObservation?

func observe(_ button: UIButton, expectation:


XCTestExpectation) {
token = button
.observe(\.titleLabel?.text, options: [.new]) { _, _ in
expectation.fulfill()
}
}

deinit {
token?.invalidate()
}
}

ButtonObserver observes a UIButton for changes to its titleLabel’s text by using


Key-Value Observing. When the text changes, a callback is made to
observeValue(forKeyPath:of:change:context:). This object holds on to the
supplied XCTestExpectation and fulfills it in that callback.

Next, open StepCountControllerTests.swift add the following test helpers under //


MARK: - When:

func whenCaught() {
AppModel.instance.setToCaught()
}

func whenCompleted() {
AppModel.instance.setToComplete()
}

raywenderlich.com 89
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Build and run the StepCountControllerTests tests, and you’ll see a couple failures
in the console:

XCTAssertEqual failed: ("Optional("Pause")") is not equal to


("Optional("Try Again")")

XCTAssertEqual failed: ("Optional("Pause")") is not equal to


("Optional("Start Over")")

The button titles aren’t updating when whenCaught() and whenCompleted() are
called in your test, because there aren’t yet any hooks in the production code to do
this. Fix that by adding the following to viewDidLoad in
StepCountController.swift:

AppModel.instance.stateChangedCallback = { model in
DispatchQueue.main.async {
self.updateUI()
}
}

stateChangedCallback is now used to update the UI when appState is updated in


the model. Now the tests will pass and you’re ready to move on.

Note: Stopping execution in the debugger doesn’t pause the wait timeout.
You just added a bunch of code, and if there was a mistake you might go back
and debug the problem. This is common when writing tests, especially when
they do not behave as expected. When the debugger pauses at a breakpoint
and you explore for the logic error, be mindful that the test will probably fail
due to timeout. Simply disable or remove the breakpoint and re-run once the
issue is corrected.

Waiting for notifications


In the next phase of app building, you’ll add a feature to visually notify the users
when an event happens, such as meeting a milestone goal or when Nessie catches
up.

In addition to fulfilling expectations in arbitrary callbacks, there is also a feature that


allows the test to wait for User Notifications.

raywenderlich.com 90
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Building the alert center


One important feature for an activity app or game is to update the user when
important events happen. In FitNess these updates are managed by an AlertCenter.
When something interesting happens, the code will post Alerts to the AlertCenter.
The alert center is responsible for managing a stack of messages to display to the
user.

AlertCenter uses Notifications to communicate with the view controllers which


handle the alerts on screen. Because this happens asynchronously, it’s a good case to
test using XCTestExpectation.

A stub implementation of AlertCenter and AlertCenterTests have been added to


the project to speed things up.

To test out the notification behavior, open AlertCenterTests.swift and add the
following test:

func testPostOne_generatesANotification() {
// given
let exp = expectation(
forNotification: AlertNotification.name,
object: sut,
handler: nil)
let alert = Alert("this is an alert")

// when
sut.postAlert(alert: alert)

// then
wait(for: [exp], timeout: 1)
}

expectation(forNotification:object:handler:) creates an expectation that


fulfills when a notification posts. In this case, when AlertNotification.name is
posted to sut, the expectation is fulfilled. The test then posts a new Alert and waits
for that notification to be sent.

raywenderlich.com 91
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Note that it’s not generally a good idea to use a wait as the test assertion. It’s better
to use an explicit assert call. wait only tests that an expectation was fulfilled and
does not make any claims about the app’s logic. You’ll test the contents of the
notification a little later in this chapter.

Build and test, and this test will fail. If you look at the error in the console, you’ll see
a timeout failure:

Asynchronous wait failed: Exceeded timeout of 1 seconds, with


unfulfilled expectations: "Expect notification 'Alert' from
FitNess.AlertCenter".

Time to implement the application code to fix this! Open AlertCenter.swift, replace
the stub implementation of postAlert(alert:) with the following:

func postAlert(alert: Alert) {


let notification = Notification(
name: AlertNotification.name,
object: self)
notificationCenter.post(notification)
}

This creates and posts the Notification your test is listening for. Note that the passed
alert isn’t used currently, but you’ll circle back to this later.

Build and test. And the test will pass! :]

Waiting for multiple events


Next, try testing if posting two alerts sends two notifications. Add the following to
the end of AlertCenterTests:

func testPostingTwoAlerts_generatesTwoNotifications() {
//given
let exp1 = expectation(
forNotification: AlertNotification.name,
object: sut,
handler: nil)
let exp2 = expectation(
forNotification: AlertNotification.name,
object: sut,
handler: nil)
let alert1 = Alert("this is the first alert")
let alert2 = Alert("this is the second alert")

// when
sut.postAlert(alert: alert1)

raywenderlich.com 92
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

sut.postAlert(alert: alert2)

// then
wait(for: [exp1, exp2], timeout: 1)
}

This creates two expectations waiting for AlertNotification.name, posts two


different alerts, and waits for both alerts to notify.

Build and test, and it will pass. However, this test is a little naïve. To see how, delete
this line:

sut.postAlert(alert: alert2)

Now you’re only posting one of the two alerts tied to expectations the wait requires.

Test again, and it will still pass! This is because the two expectations are expecting
the same thing. They run in parallel—they don’t stack. So as soon as one alert is
posted, both expectations are fulfilled.

To solve this conundrum, you can use notification expectation’s


expectedFulfillmentCount property refine the fulfillment condition. Replace
testPostingTwoAlerts_generatesTwoNotifications() with the following:

func testPostingTwoAlerts_generatesTwoNotifications() {
//given
let exp = expectation(
forNotification: AlertNotification.name,
object: sut,
handler: nil)
exp.expectedFulfillmentCount = 2
let alert1 = Alert("this is the first alert")
let alert2 = Alert("this is the second alert")

// when
sut.postAlert(alert: alert1)

// then
wait(for: [exp], timeout: 1)
}

Setting expectedFulfillmentCount to two means the expectation won’t be met


until fulfill() has been called twice before the timeout.

raywenderlich.com 93
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Run the test, and you’ll see it fails because you only called postAlert once. This is
good proof your test is working as expected!

In the when section, add back the second postAlert under sut.postAlert(alert:
alert1):

sut.postAlert(alert: alert2)

Run the test again, and you’ll see it pass.

Expecting something not to happen


Good test suites not only test when things happen according to plan, but also check
that certain side effects do not occur. One of things the app should not do is spam
the user with alerts. Therefore, if a specific alert is posted twice, it should only
generate one notification.

And of course, you can test for this scenario. Add the following test:

func testPostDouble_generatesOnlyOneNotification() {
//given
let exp = expectation(
forNotification: AlertNotification.name,
object: sut,
handler: nil)
exp.expectedFulfillmentCount = 2
exp.isInverted = true
let alert = Alert("this is an alert")

// when
sut.postAlert(alert: alert)
sut.postAlert(alert: alert)

// then
wait(for: [exp], timeout: 1)
}

This is almost exactly like the last one, except for this line:

exp.isInverted = true

raywenderlich.com 94
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

When an expectation is inverted it indicates this test fails if the expectation is


fulfilled and succeeds if the wait times out. Put another way, this test will fail if two
notifications are triggered by the two alerts.

Right now, the test fails because the application code currently allows multiple alerts
to post.

Open AlertCenter.swift. Add the following instance variable:

private var alertQueue: [Alert] = []

The alertQueue will be an important part of AlertCenter. It will help manage a


potentially large stack of messages for the user, as they can accumulate in the
background.

Next add the following statements to the top of postAlert(alert:):

guard !alertQueue.contains(alert) else { return }

alertQueue.append(alert)

If the same alert is passed to postAlert(alert:) twice, the second one will be
ignored.

Build and test again. All green!

Be sure to run all the tests from time to time to make sure fixes for one test don’t
break another.

Showing the alert to a user


In the app’s architecture, the RootViewController is responsible for showing alerts
to the user via its alertContainer view.

Create a new Unit Test Case Class file in the App Layer folder, under Cases. Name
it RootViewControllerTests.swift.

Add the following import:

@testable import FitNess

raywenderlich.com 95
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Next, replace the test boilerplate in the class with:

var sut: RootViewController!

override func setUpWithError() throws {


try super.setUpWithError()
sut = getRootViewController()
}

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}

Finally, add a test for the base condition: that is, when the view controller is loaded,
there are no alerts showing:

// MARK: - Alert Container

func testWhenLoaded_noAlertsAreShown() {
XCTAssertTrue(sut.alertContainer.isHidden)
}

Run this and confirm it passes.

Next, add the following to test that the alert container is shown when there is an
alert:

func testWhenAlertsPosted_alertContainerIsShown() {
// given
let exp = expectation(
forNotification: AlertNotification.name,
object: nil,
handler: nil)
let alert = Alert("show the container")

// when
AlertCenter.instance.postAlert(alert: alert)

// then
wait(for: [exp], timeout: 1)
XCTAssertFalse(sut.alertContainer.isHidden)
}

raywenderlich.com 96
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

An expectation will be fulfilled by AlertNotification.name and


postAlert(alert:) is called to ultimately trigger the notification. After waiting for
the expectation, XCTAssertFalse checks the alertContainer is visible.

Now it’s time to get the test to pass by adding the code to show the alert. Go back to
RootViewController.swift and add the following at the bottom of viewDidLoad:

AlertCenter.listenForAlerts { center in
self.alertContainer.isHidden = false
}

AlertCenter.listenForAlerts(_:) is a helper method that you’ll create to


register for alert notifications, and run the passed closure. The closure will unhide
the alertContainer when triggered.

Open AlertCenter.swift, find the “class helpers” extension and add the following:

class func listenForAlerts(


_ callback: @escaping (AlertCenter) -> Void
) {
instance.notificationCenter
.addObserver(
forName: AlertNotification.name,
object: instance,
queue: .main) { _ in
callback(instance)
}
}

listenForAlerts(_:) adds AlertCenter as an observer for the


AlertNotification.name notification that triggers the callback. This will result in
alertContainer displaying in RootViewController.

Build and run your new test and it should now pass.

Continuous refactoring
When you only run testWhenLoaded_noAlertsAreShown(), it will pass. If you run
all the tests in RootViewControllerTests, then
testWhenLoaded_noAlertsAreShown() may fail.

That is because the sut state is tied to the running UIApplication and is preserved
between runs. If testWhenAlertsPosted_alertContainerIsShown() runs first and
displays the alert, it will still be there when testWhenLoaded_noAlertsAreShown()
checks if any are displayed.

raywenderlich.com 97
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

To resolve this issue, you’ll refactor the code and build a way to clear out all the
alerts and reset the view between tests.

First, you need an interface to the state of AlertCenter. Add the following test to
AlertCenterTests.swift:

// MARK: - Alert Count


func testWhenInitialized_AlertCountIsZero() {
XCTAssertEqual(sut.alertCount, 0)
}

This means that AlertCenter needs an alertCount variable for the test to compile.
Add the following property to the class in AlertCenter.swift:

var alertCount: Int {


return alertQueue.count
}

Build and test testWhenInitialized_AlertCountIsZero() and you’ll see it now


passes.

When adding new functionality, it’s important to cover the basic conditions as well.
Add the following to AlertCenterTests.swift:

func testWhenAlertPosted_CountIsIncreased() {
// given
let alert = Alert("An alert")

// when
sut.postAlert(alert: alert)

// then
XCTAssertEqual(sut.alertCount, 1)
}

func testWhenCleared_CountIsZero() {
// given
let alert = Alert("An alert")
sut.postAlert(alert: alert)

// when
sut.clearAlerts()

// then
XCTAssertEqual(sut.alertCount, 0)
}

testWhenAlertPosted_CountIsIncreased() tests that posting an alert increases


the alertCount you added for the prior test.

raywenderlich.com 98
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

testWhenCleared_CountIsZero() tests a new method, clearAlerts(), which you


need to create. First, you’ll want to run it in tearDownWithError, by adding the
following to the top of the method:

AlertCenter.instance.clearAlerts()

Because AppModelTests indirectly mess with DataModel state, they can also trigger
alerts that need to be cleared. Open AppModelTests.swift, add the following to the
top of tearDownWithError:

AlertCenter.instance.clearAlerts()

This ensures the state of AlertCenter is reset after each test that modifies it. Open
AlertCenter.swift, add the following to AlertCenter:

// MARK: - Alert Handling

func clearAlerts() {
alertQueue.removeAll()
}

This allows you to remove all alerts from alertQueue, which can be used to solve
your issues with persisted alerts between tests. But first, there is one more place you
need to use your new alertCount.

Go back to RootViewController.swift and change the listenForAlerts callback


block in viewDidLoad to:

self.alertContainer.isHidden = center.alertCount == 0

Now when an alert is triggered, you display alertContainer only if more than one
alert is currently present. Are you dizzy yet? With TDD, adding functionality requires
looping back and forth between the application and tests code.

Finally, you can fix the broken testWhenLoaded_noAlertsAreShown by adding to


the top of tearDownWithError in RootViewControllerTests.swift:

AlertCenter.instance.clearAlerts()

Now alertQueue will clear after each test, preventing tests that modify the queue
from impacting each other.

raywenderlich.com 99
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

With the count reset, you just need to clear any existing alerts at the start of each
test to avoid the persistence issue you observed in
testWhenLoaded_noAlertsAreShown(). Add the following to the bottom of
setUpWithError:

sut.reset()

Now all the tests will pass, regardless of execution order.

If you want to see the alert view in practice, temporarily replace


startStopPause(_:) in StepCountController.swift with the following:

@IBAction func startStopPause(_ sender: Any?) {


let alert = Alert("Test Alert")
AlertCenter.instance.postAlert(alert: alert)
}

Now it’ll display an alert for any state change. Build and run. When the app loads tap
Start.

For now undo those changes and move on for more expectation testing.

raywenderlich.com 100
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Getting specific about notifications


To make sure the UI is updated effectively, it will be useful to add additional
information to the alert notification beyond the name.

In particular, it will be useful to add the associated Alert to the notification’s


userInfo.

Open AlertCenterTests.swift and add the following to AlertCenterTests:

// MARK: - Notification Contents


func testNotification_whenPosted_containsAlertObject() {
// given
let alert = Alert("test contents")
let exp = expectation(
forNotification: AlertNotification.name,
object: sut,
handler: nil)

var postedAlert: Alert?


sut.notificationCenter.addObserver(
forName: AlertNotification.name,
object: sut,
queue: nil) { notification in
let info = notification.userInfo
postedAlert = info?[AlertNotification.Keys.alert] as? Alert
}

// when
sut.postAlert(alert: alert)

// then
wait(for: [exp], timeout: 1)
XCTAssertNotNil(postedAlert, "should have sent an alert")
XCTAssertEqual(
alert,
postedAlert,
"should have sent the original alert")
}

In addition to using a notification expectation, this test also sets up an additional


listener for an AlertNotification. In the observation closure, the Alert that is
expected to be in the userInfo is stored so it can be compared in the test assert.

raywenderlich.com 101
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Note: While you should strive for a single assert per test, it’s OK to have more
than one if they both confirm the same truth. In this case, you’re trying to
validate that the notification contains the same Alert object that was posted.
Checking that the notification’s alert isn’t nil is part of that validation, as is
comparing it to the posted alert.

To get this test to pass, you have to add the alert object to the notification. Open
AlertCenter.swift change the let notification = ... line in
postAlert(alert:) to:

let notification = Notification(


name: AlertNotification.name,
object: self,
userInfo: [AlertNotification.Keys.alert: alert])

This adds the posted alert object to the notification so it can be observed in the test’s
closure. Now run testNotification_whenPosted_containsAlertObject() and
you should see another green test.

Driving alerts from the data model


In order to drive engagement and give the user a sense of fulfillment as they near
their goal, it’s important to present messages to the user as they reach certain
milestones.

To start off on a positive note, encourage the user by giving them alerts at certain
milestones. When they reach 25%, 50%, and 75% of the goal, they should see an
encouragement alert, and at 100% a congratulations alert.

There are already some hard coded values for these in an Alert extension.

Before writing the next set of tests, create a new helper file. Under the Test
Extensions group add a new group, Alerts. Then add a new Swift file named
Notification+Tests.swift.

raywenderlich.com 102
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Add the following code to the new file, below the Foundation import:

@testable import FitNess

extension Notification {
var alert: Alert? {
return userInfo?[AlertNotification.Keys.alert] as? Alert
}
}

This helper extension will make it easier to get the Alert object out of the
notification. You can be fairly confident this works because
testNotification_whenPosted_containsAlertObject() tested similarly built
userInfo. You could also go back and update that test to use this new helper. TDD
For The Win!

Now you can start writing tests to check that milestone notifications are generated.

Open DataModelTests.swift add the following test to the end of DataModelTests:

// MARK: - Alerts
func testWhenStepsHit25Percent_milestoneNotificationGenerated()
{
// given
sut.goal = 400
let exp = expectation(
forNotification: AlertNotification.name,
object: nil) { notification -> Bool in
return notification.alert == Alert.milestone25Percent
}

// when
sut.steps = 100

// then
wait(for: [exp], timeout: 1)
}

In this test, the optional handler closure is used when setting up the expectation.
The closure takes the Notification as input and returns a Bool indicating whether
or not the expectation should be fulfilled. Here you only fulfill the expectation when
the alert is a .milestone25Percent. With the goal set to 400, setting steps to 100
should trigger that alert and fulfill your expectation.

To make this pass, you’ll need to update DataModel to trigger the 25 percent alert
when appropriate.

raywenderlich.com 103
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

First open DataModel.swift. Next, replace the steps var with the following:

var steps: Int = 0 {


didSet {
updateForSteps()
}
}

Now changes in the step count will trigger updateForSteps(), which will post
necessary milestone alerts.

Finally, add the following method below restart():

// MARK: - Updates due to distance


func updateForSteps() {
guard let goal = goal else { return }
if Double(steps) >= Double(goal) * 0.25 {
AlertCenter.instance.postAlert(
alert: Alert.milestone25Percent)
}
}

Now when steps hit 25% of the goal, you post Alert.milestone25Percent. Build
and test testWhenStepsHit25Percent_milestoneNotificationGenerated() and
it will pass when the alert is generated.

Previous tests let you know that because the alert is generated it will be shown to the
user. You’ll have to wait for the next chapter to see the actual step counter in action.

On your own, add three more tests: one each for 50%, 75%, and 100% of completion
with a goal of 400:

• 50%: Use Alert.milestone50Percent and steps = 200 for the when condition.

• 75%: Use Alert.milestone75Percent and steps = 300 for the when condition.

• 100%: Use Alert.goalComplete and steps = 400 for the when condition.

Duplicate the if statement in updateForSteps for each of these conditions to get


the tests to pass. With these separate if statements, updateForSteps will post all
alerts up to the current threshold when triggered; you shouldn’t address that issue
yet. You’ll also need to add AlertCenter.instance.clearAlerts() to the test’s
tearDownWithError to flush out the alert queue each time.

raywenderlich.com 104
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Testing for multiple expectations


Your new milestone notification tests all seem pretty similar. This is an indicator
that you should refactor them to reduce repeated code.

Still in DataModelTests.swift, add a new method under // MARK: - Given:

func givenExpectationForNotification(
alert: Alert
) -> XCTestExpectation {
let exp = expectation(
forNotification: AlertNotification.name,
object: nil) { notification -> Bool in
return notification.alert == alert
}
return exp
}

This helper method creates an expectation that waits for a notification containing
the passed alert. Next, refactor
testWhenStepsHit25Percent_milestoneNotificationGenerated() to use this
helper. Replace the expectation definition with the following:

let exp =
givenExpectationForNotification(alert: .milestone25Percent)

Do the same for the other three milestone tests.

Now you can write a test that checks that all of these alerts are generated, each in
order.

Add the following test to DataModelTests:

func testWhenGoalReached_allMilestoneNotificationsSent() {
// given
sut.goal = 400
let expectations = [
givenExpectationForNotification(alert: .milestone25Percent),
givenExpectationForNotification(alert: .milestone50Percent),
givenExpectationForNotification(alert: .milestone75Percent),
givenExpectationForNotification(alert: .goalComplete)
]

// when
sut.steps = 400

// then
wait(for: expectations, timeout: 1, enforceOrder: true)
}

raywenderlich.com 105
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

So far you’ve been using wait(for:timeout:) with an array of just one expectation.
Here you can see why accepting an array is useful. It allows you to provide multiple
expectations and wait for all of them to be fulfilled.

Also shown here is the optional enforceOrder parameter. This makes sure not only
that all the expectations are fulfilled but that those fulfillments happen in the order
specified by the input array.

The ordering check allows for sophisticated tests. For example, you could use this
when writing a test for a multi-step process like image filtering or a network login
that requires multiple API calls (like OAuth or SAML). These tests not only ensure all
the steps happen in the necessary order in production code, but also validate that
your test code isn’t going through a different flow than expected.

Refining Requirements
The previous set of unit tests have one flaw when it comes to validating the app.
They test a snapshot of the app’s state and do not consider that the app is dynamic.

When in progress, the app will continually update the step count, and it’s important
to not spam the user at each step, but instead only alert them when a threshold is
first crossed. In addition, the user has the option to clear the alerts, so the guard
added to postAlert(alert:) won’t prevent a repeat alert if an earlier alert was
cleared by the user.

Always testing first, open AlertCenterTests.swift and add this to the bottom of
AlertCenterTests:

// MARK: - Clearing Individual Alerts


func testWhenCleared_alertIsRemoved() {
// given
let alert = Alert("to be cleared")
sut.postAlert(alert: alert)

// when
sut.clear(alert: alert)

// then
XCTAssertEqual(sut.alertCount, 0)
}

This tests that if an alert is added and then cleared, there are no alerts left in the
AlertCenter.

raywenderlich.com 106
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

To pass the test, add the following method to the “Alert Handling” section of
AlertCenter.swift:

func clear(alert: Alert) {


if let index = alertQueue.firstIndex(of: alert) {
alertQueue.remove(at: index)
}
}

This removes the passed alert from the alertQueue. Run your tests and they
should all pass again.

Next, open DataModelTests.swift and add the following:

func testWhenStepsIncreased_onlyOneMilestoneNotificationSent() {
// given
sut.goal = 10
let expectations = [
givenExpectationForNotification(alert: .milestone25Percent),
givenExpectationForNotification(alert: .milestone50Percent),
givenExpectationForNotification(alert: .milestone75Percent),
givenExpectationForNotification(alert: .goalComplete)
]

// clear out the alerts to simulate user interaction


let alertObserver = AlertCenter.instance.notificationCenter
.addObserver(
forName: AlertNotification.name,
object: nil,
queue: .main) { notification in
if let alert = notification.alert {
AlertCenter.instance.clear(alert: alert)
}
}

// when
for step in 1...10 {
self.sut.steps = step
sleep(1)
}

// then
wait(for: expectations, timeout: 20, enforceOrder: true)
AlertCenter.instance.notificationCenter
.removeObserver(alertObserver)
}

raywenderlich.com 107
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

This is your busiest test yet, and it has a few parts:

• The given section sets up a sequence of milestone alert expectations.

• A separate observer watches for alerts and clears them from the AlertCenter.
This ensures that repeated notifications don’t get ignored because they haven’t
yet been dismissed by the user.

• The when section increments steps to generate the alerts by crossing a series of
the milestones individually. Using sleep or equivalent in tests should only be
done sparingly as this drastically increases the test time. It’s necessary here to give
time for the notifications to post and be cleared.

• The then section uses wait to test that the expectations are fulfilled as expected.
At the end of the test, you remove alertObserver to prevent it from impacting
other tests.

Right now the test will pass, which violates the TDD step of writing a failing test
first. That’s because right now it’s not enforcing that there should be a single
notification per milestone. That has to be done in the expectation itself.

Still in DataModelTests.swift, replace


givenExpectationForNotification(alert:) with the following:

func givenExpectationForNotification(
alert: Alert) -> XCTestExpectation {

let exp = XCTNSNotificationExpectation(


name: AlertNotification.name,
object: AlertCenter.instance,
notificationCenter: AlertCenter.instance.notificationCenter)
exp.handler = { notification -> Bool in
return notification.alert == alert
}
exp.expectedFulfillmentCount = 1
exp.assertForOverFulfill = true
return exp
}

This ditches the convenience method in order to create an


XCTNSNotificationExpectation, which is a XCTestExpectation with more
notification specific features. You set the expectedFulfillmentCount and
assertForOverFulfill which will generate an assertion if the expectation is
fulfilled more than the count.

Now the test will fail as a single alert is repeated for multiple steps. To get the test to
pass, DataModel has to be modified to keep track of sent alerts.

raywenderlich.com 108
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Open DataModel.swift and add the following to the top of the class:

// MARK: - Alerts
var sentAlerts: [Alert] = []

Next, replace updateForSteps() with the following:

private func checkThreshold(percent: Double, alert: Alert) {


guard !sentAlerts.contains(alert),
let goal = goal else {
return
}
if Double(steps) >= Double(goal) * percent {
AlertCenter.instance.postAlert(alert: alert)
sentAlerts.append(alert)
}
}

func updateForSteps() {
checkThreshold(percent: 0.25, alert: .milestone25Percent)
checkThreshold(percent: 0.50, alert: .milestone50Percent)
checkThreshold(percent: 0.75, alert: .milestone75Percent)
checkThreshold(percent: 1.00, alert: .goalComplete)
}

This cleans up the code a little bit and now checks not just that the threshold was
crossed but also that an alert wasn’t already sent. This way if a user crosses a
threshold and dismisses the alert, they won’t see that same alert again.

Finally, add the following to the end of restart():

sentAlerts.removeAll()

This ensures that a restart clears out your alerts. Build and run, and the tests should
all pass!

Using other types of expectations


The bulk of the time you’re testing asynchronous processes, you’ll use a regular
XCTestExpectation. XCTNSNotificationExpectation covers most other needs.
For specific uses, there are two other stock expectations: XCTKVOExpectation and
XCTNSPredicateExpectation.

These look for their eponymous conditions: KVO expectations observe changes to a
keyPath and predicate expectations wait for their predicate to be true.

raywenderlich.com 109
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

There’s one place where you’ve already used KVO for an expectation, and that’s with
the ButtonObserver found in StepCountControllerTests.swift. You can replace
that helper class completely using a KVO based XCTestExpectation. Rather than
using the more fully featured XCTKVOExpectation, you’ll use a special
XCTestExpectation initializer that provides KVO capabilities.

Delete ButtonObserver.swift. Next, open StepCountControllerTests.swift and add


this method in the given section:

func expectTextChange() -> XCTestExpectation {


return keyValueObservingExpectation(
for: sut.startButton as Any,
keyPath: "titleLabel.text")
}

This helper creates an expectation on startButton that observes the keyPath


titleLabel.text. The same keyPath was used in the old ButtonObserver. This
method accepts an optional handler block where you would check the observation
to see if it meets the expectation. For these tests, only the first change needs to be
observed, so you don’t supply the handler to filter fulfillment.

Next, in testController_whenCaught_buttonLabelIsTryAgain() and


testController_whenComplete_buttonLabelIsStartOver() replace the let exp
= ... and two observer lines with the following:

let exp = expectTextChange()

And change the waitForExpectations lines to:

wait(for: [exp], timeout: 1)

Build and test and the tests will pass as if nothing happened!

raywenderlich.com 110
iOS Test-Driven Development by Tutorials Chapter 5: Test Expectations

Challenge
This tutorial only scratched the surface of testing asynchronous functions. Here are
some things to add to the app with test coverage:

• Add AlertCenter tests addressing edge cases for clearing alerts such as clearing
an empty queue and clearing the same alert multiple times.

• Create tests for AlertViewController. Test that the text used for alertLabel’s
updates to reflect a new alert, and that it uses the proper color for the given
severity. This requires adding the ability to get the first alert out of the
AlertCenter, and updating tests around that as well.

• It wouldn’t be fair to the user if they didn’t get a warning of Nessie’s progress. Add
tests in DataModelTests for Nessie catching up to 50% and then to 90%.

Key points
• Use XCTestExpectation and its subclasses to make tests wait for asynchronous
process completion.

• Test expectations help test properties of the asynchronicity, like order and number
of occurrences, but XCTAssert functions should still be used to test state.

Where to go from here?


So much app code is asynchronous by nature—disk and network access, UI events,
system callbacks, and so on. It’s important to understand how to test that code, and
this chapter gives you a good start. Many popular 3rd party testing frameworks also
have functions that make writing these types of tests easier. For example
Quick+Nimble (https://github.com/Quick/Nimble#asynchronous-expectations)
allows you to write an assert, expectation and wait in one line:

expect(alerts).toEventually(contain(alert1, alert2))

Alternatively if your app uses a framework like RxSwift (https://github.com/


ReactiveX/RxSwift) then you can use their RxBlocking and RxTest frameworks. See
this tutorial (https://www.raywenderlich.com/7408-testing-your-rxswift-code) for
more information.

raywenderlich.com 111
6 Chapter 6: Dependency
Injection & Mocks
By Michael Katz

So far, you’ve built and tested a fair amount of the app. There is one gigantic hole
that you may have noticed… this “step-counting app” doesn’t yet count any steps!

In this chapter, you’ll learn how to use mocks to test code that depends on system or
external services without needing to call services — the services may not be
available, usable or reliable. These techniques allow you to test error conditions, like
a failed save, and to isolate logic from SDKs, like Core Motion and HealthKit.

Don’t have an iPhone handy? Don’t worry; you’ll dip into functional testing using
the Simulator to handle mock data.

raywenderlich.com 112
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

What’s up with fakes, mocks, and stubs?


When writing tests, it’s important to isolate the SUT from other parts of the code so
your tests have high confidence that they’re testing the system as described. Tests
focused on edge cases or error conditions can be very difficult to write, as they often
involve specific state external to the SUT. It’s also difficult to diagnose and debug
tests that fail due to intermittent or inconsistent issues outside the SUT.

The way to isolate the SUT and circumvent these issues is to use test doubles:
objects that stands in for real code. There are several variants of test doubles:

• Stub: Stubs stand in for the original object and provide canned responses. These
are often used to implement one method of a protocol and have empty or nil
returning implementations for the others.

• Fake: Fakes often have logic, but instead of providing real or production data, they
provide test data. For example, a fake network manager might read/write from
local JSON files instead of connecting over a network.

• Mock: Mocks are used to verify behavior, that is they should have an expectation
that a certain method of the mock gets called or that its state was set to an
expected value. Mocks are generally expected to provide test values or behaviors.

• Partial mock: While a regular mock is a complete substitution for a production


object, a partial mock uses the production code and only overrides part of it to test
the expectations. Partial mocks are usually a subclass or provide a proxy to the
production object.

Understanding CMPedometer
There’s a few ways of gathering activity data from the user, but the CMPedometer API
in Core Motion is by far the easiest.

Using a CMPedometer is easy as:

1. Check that the pedometer is available and the user has granted permission.

2. Start listening for updates.

3. Gather step and distance updates until the user pauses, completes the goal or
loses to Nessie.

raywenderlich.com 113
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

The pedometer object is supplied a CMPedometerHandler, which has a single


callback that receives CMPedometerData (or an error). This data object has the step
count and distance travelled.

Here’s the thing… you’re using TDD so using a CMPedometer is tricky, even if you
have the host app run on a physical device. CMPedometer depends on the device
state, which is too variable for consistent unit tests.

Give it a try. First, navigate to and open the starter project. Next, open
PedometerTests.swift which has been added to the Data Model test case group.
Next add the following below tearDownWithError():

func testCMPedometer_whenQueries_loadsHistoricalData() {
// given
var error: Error?
var data: CMPedometerData?
let exp = expectation(description: "pedometer query returns")

// when
let now = Date()
let then = now.addingTimeInterval(-1000)
sut.queryPedometerData(
from: then,
to: now) { pedometerData, pedometerError in
error = pedometerError
data = pedometerData
exp.fulfill()
}

// then
wait(for: [exp], timeout: 1)
XCTAssertNil(error)
XCTAssertNotNil(data)
if let steps = data?.numberOfSteps {
XCTAssertGreaterThan(steps.intValue, 0)
} else {
XCTFail("no step data")
}
}

This test creates an expectation for a returned pedometer query, calls


queryPedometerData(from:to:) to query the data and fulfill the expectation. It
then asserts that the data contains at least one step.

raywenderlich.com 114
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Although this test compiles, it crashes on launch. Apple requires permission to use
Core Motion. Strike #1 against using a real CMPedometer object in the tests. In order
to ask for permission, a usage description is required. Open the app’s Info.plist.

Add a new row, use the key Privacy - Motion Usage Description and set the value
to “Pedometer access is required to gather step and distance information.”

Build and test, and it may fail depending on if you run the app on device or
Simulator, and if you’ve accepted the permission pop-up or not. The unpredictability
caused by lack of control over CMPedometer makes this a pretty poor test. This
sounds like a job for a mock!

Delete the PedometerTests.swift test file; you’re about do much better.

Mocking
Restating the problem
Open AppModelTests.swift, and add the following test below // MARK: -
Pedometer:

func testAppModel_whenStarted_startsPedometer() {
//given
givenGoalSet()
let predicate = NSPredicate { model, _ -> Bool in
(model as? AppModel)?.pedometerStarted ?? false
}
let exp = expectation(
for: predicate,
evaluatedWith: sut,
handler: nil)

raywenderlich.com 115
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

// when
try! sut.start()

// then
wait(for: [exp], timeout: 1)
XCTAssertTrue(sut.pedometerStarted)
}

This test intends to verify that starting the app model will also start the pedometer.
If you read the previous chapter, you’ll recognize the elusive
XCTNSPredicateExpectation used to wait for the status change.

This test is subtly different from the previous one: It doesn’t test the pedometer
object directly. Instead, the test verifies the behavior of the SUT by measuring the
effect on the pedometer (as exposed through pedometerStarted).

To get this compiling, you’ll need to modify AppModel. Open AppModel.swift, add
the following two vars:

let pedometer = CMPedometer()


private(set) var pedometerStarted = false

This adds a little state to keep track of the pedometer.

Next, add the following to the end of start():

startPedometer()

Finally, add the following extension to the bottom of the file:

// MARK: - Pedometer
extension AppModel {
func startPedometer() {
pedometer.startEventUpdates { _, error in
if error == nil {
self.pedometerStarted = true
}
}
}
}

raywenderlich.com 116
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

This uses the pedometer event handler callback to determine if the pedometer has
started. With a CMPedometer, you can’t write a simple test to check if it’s started as
that state isn’t exposed in the API. However, this callback will be called soon after
starting event updates. If step counting is available, then there won’t be an error, and
you’ll know it’s started.

Build and test, and this will pass if you run it on a device and have granted
permission to motion data. If you run on Simulator or device without this permission
granted, it’ll fail.

Mocking the pedometer


To move pass this impasse, it’s time to create the mock pedometer. In order to swap
CMPedometer for it’s mock object, you’ll first need to separate the pedometer’s
interface from its implementation.

To do that, you’ll make use of two classic patterns: Facade and Bridge.

First, create a new group in the app, named Pedometer. In that group, create a new
Swift file, Pedometer.swift.

For now, just add the following code:

protocol Pedometer {
func start()
}

This is the start of the Bridge protocol that will allow you to substitute any
pedometer implementation for the real one.

In order to do that, you’ll have to declare conformance for CMPedometer. Create


another Swift file in the group: CMPedometer+Pedometer.swift and replace its
contents with the following:

import CoreMotion

extension CMPedometer: Pedometer {


func start() {
startEventUpdates { _, _ in
// do nothing here for now
}
}
}

This declares conformance to the new protocol and migrates the start behavior you
implemented in startPedometer. It doesn’t do anything much yet, but will soon.

raywenderlich.com 117
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Next, open AppModel.swift and decouple AppModel from the specific


implementation of CMPedometer:

1. Change the pedometer declaration to: let pedometer: Pedometer.

2. Remove the pedometerStarted property.

3. Add the following initializer:

init(pedometer: Pedometer = CMPedometer()) {


self.pedometer = pedometer
}

4. Change startPedometer to:

func startPedometer() {
pedometer.start()
}

The optional init parameter is where you’ll be able to replace the default
CMPedometer with the mock object. The reduction of code in startPedometer is the
advantage of using a Facade: You can hide the specific complexity of the
CMPedometer behind a simplified interface.

Now, it’s time to create the mock!

Create a new Swift file in the Mocks group in FitNessTests named


MockPedometer.swift and replace its contents with the following:

import CoreMotion
@testable import FitNess

class MockPedometer: Pedometer {


private(set) var started: Bool = false

func start() {
started = true
}

raywenderlich.com 118
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

This creates a very different implementation of Pedometer. Its start method


instead of making CoreMotion calls just sets a Bool that can be checked in a test.
Here’s another value of mocking — you can spy or inspect the mock to check that the
right methods were called or that its state was set appropriately.

Now, go back to AppModelTests.swift and add the following property up top and
update setUpWithError:

var mockPedometer: MockPedometer!

override func setUpWithError() throws {


try super.setUpWithError()
mockPedometer = MockPedometer()
sut = AppModel(pedometer: mockPedometer)
}

This creates a mock pedometer and uses it when creating the sut.

Now, go back to testAppModel_whenStarted_startsPedometer and replace it with


the following:

func testAppModel_whenStarted_startsPedometer() {
//given
givenGoalSet()

// when
try! sut.start()

// then
XCTAssertTrue(mockPedometer.started)
}

This simplified test now tests the side effect of start on the mock object. In addition
to being a simpler test, it’s guaranteed to pass regardless of the device state. Build
and test, and you’ll see that it passes.

Handling error conditions


Mocks make it easy to test error conditions. If you’ve been following along so far
using both Simulator and a device, you may have encountered one or both of these
error states:

• Step counting is not available on a device, such as the Simulator.

• The user may deny permission for motion recording on device.

raywenderlich.com 119
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Dealing with no pedometer


To handle the first case, you’ll have to add functionality to detect that the pedometer
is not available and to inform the user.

First, add this test in AppModelTests under the “Pedometer” mark:

func testPedometerNotAvailable_whenStarted_doesNotStart() {
// given
givenGoalSet()
mockPedometer.pedometerAvailable = false

// when
try! sut.start()

// then
XCTAssertEqual(sut.appState, .notStarted)
}

This simple check just makes sure the app state doesn’t proceed to inProgress
when the pedometer isn’t available.

Next, open Pedometer.swift and add the following to the protocol definition:

var pedometerAvailable: Bool { get }

This creates a var to read the availability state.

Next, open MockPedometer.swift and update MockPedometer by adding the


following:

var pedometerAvailable: Bool = true

And for the real implementation — to be used by your app code — open
CMPedometer+Pedometer.swift and add the following:

var pedometerAvailable: Bool {


return CMPedometer.isStepCountingAvailable() &&
CMPedometer.isDistanceAvailable() &&
CMPedometer.authorizationStatus() != .restricted
}

You can see that the “real” implementation is a lot more interesting, but not
controllable.

raywenderlich.com 120
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Now the test compiles, and it’s time to get it to pass.

Open AppModel.swift, find start() and add the following before appState
= .inProgress:

guard pedometer.pedometerAvailable else {


AlertCenter.instance.postAlert(alert: .noPedometer)
return
}

Unlike the other guard statement, this condition doesn’t raise an exception; instead,
it uses the new AlertCenter way of communicating with the user. The resulting
error handling, where start() is called, will be a little different, and refactoring it is
out of scope of this chapter.

Build and test, and it will pass now, as the new guard prevents the appState from
progressing to inProgress when the pedometer isn’t available. Note that, if you run
the entire suite, some other tests will now fail — you’ll circle back to those in a
moment.

It’s a good idea to test the alert, as well.

Open, AppModelTests.swift and add the following below


testPedometerNotAvailable_whenStarted_doesNotStart():

func testPedometerNotAvailable_whenStarted_generatesAlert() {
// given
givenGoalSet()
mockPedometer.pedometerAvailable = false
let exp = expectation(
forNotification: AlertNotification.name,
object: nil,
handler: alertHandler(.noPedometer))

// when
try! sut.start()

// then
wait(for: [exp], timeout: 1)
}

This sets pedometerAvailable to false and waits for the corresponding alert. The
test will pass out of the gate due to the code previously added to AppModel for
displaying this alert.

raywenderlich.com 121
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Injecting dependencies
Re-run all the tests, and you will see failures in StepCountControllerTests. That’s
because this new pedometerAvailable guard in AppModel is still dependent on the
production CMPedometer in other tests.

One way to fix that this to make the pedometer into a variable so it can be modified
for testing.

Open AppModel.swift and change the let to a var:

var pedometer: Pedometer

Next, open ViewControllers.swift and add the following to the top of


getRootViewController():

AppModel.instance.pedometer = MockPedometer()

This sets the mock pedometer when the root view controller is fetched for tests,
which means any view controller test will get a mock pedometer.

Build and run all the tests, and they will now pass.

Dealing with no permission


The other error state that needs to be handled is when the user declines the
permission pop-up.

Open AppModelTests.swift and add the following to the end of the class:

func testPedometerNotAuthorized_whenStarted_doesNotStart() {
// given
givenGoalSet()
mockPedometer.permissionDeclined = true

// when
try! sut.start()

// then
XCTAssertEqual(sut.appState, .notStarted)
}

func testPedometerNotAuthorized_whenStarted_generatesAlert() {
// given
givenGoalSet()
mockPedometer.permissionDeclined = true
let exp = expectation(

raywenderlich.com 122
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

forNotification: AlertNotification.name,
object: nil,
handler: alertHandler(.notAuthorized))

// when
try! sut.start()

// then
wait(for: [exp], timeout: 1)
}

These test handling of a permissionDeclined error. The first test checks that the
app state stays in .notStarted and the second checks for a user alert.

To get them to work, you need to add permissionDeclined in a few places:

First, open Pedometer.swift, and add the following to the protocol definition:

var permissionDeclined: Bool { get }

Next, open MockPedometer.swift and add the following to the mock


implementation:

var permissionDeclined: Bool = false

Next, open CMPedometer+Pedometer.swift and add the following to the real


implementation:

var permissionDeclined: Bool {


return CMPedometer.authorizationStatus() == .denied
}

Finally, open AppModel.swift, and add another guard statement to start:

guard !pedometer.permissionDeclined else {


AlertCenter.instance.postAlert(alert: .notAuthorized)
return
}

With permissionDeclined handled, the tests will now pass.

raywenderlich.com 123
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Mocking a callback
There is another important error situation to handle. This occurs the very first time
the user taps Start on a pedometer-capable device. In that case, the start flow goes
ahead, but the user can decline in the permission pop-up. If the user declines, there
is an error in the eventUpdates callback.

Let’s test that condition. Open AppModelTests.swift and add the following to the
end of the class definition:

func testAppModel_whenDeniedAuthAfterStart_generatesAlert() {
// given
givenGoalSet()
mockPedometer.error = MockPedometer.notAuthorizedError
let exp = expectation(
forNotification: AlertNotification.name,
object: nil,
handler: alertHandler(.notAuthorized))

// when
try! sut.start()

// then
wait(for: [exp], timeout: 1)
}

Unlike the previous tests, this doesn’t explicitly set permissionDeclined, so the
model can attempt to start the pedometer. Instead, the test relies on passing an error
to the mock to generate the alert while the pedometer is starting.

The next step is to build a way to get that error back to the SUT.

Open Pedometer.swift, change the definition of start() to the following:

func start(completion: @escaping (Error?) -> Void)

This allows for a completion callback for error handling.

Next, update CMPedometer+Pedometer.swift by replacing start with:

func start(completion: @escaping (Error?) -> Void) {


startEventUpdates { _, error in
completion(error)
}
}

This forwards the error on to the completion.

raywenderlich.com 124
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Next add the error handling in AppModel.swift, by replacing startPedometer with


the following:

func startPedometer() {
pedometer.start { error in
if let error = error {
let alert = error.is(CMErrorMotionActivityNotAuthorized)
? .notAuthorized
: Alert(error.localizedDescription)
AlertCenter.instance.postAlert(alert: alert)
}
}
}

The closure checks if an error was returned when starting the pedometer. If it’s a
CMErrorMotionActivityNotAuthorized, then it posts a notAuthorized alert;
otherwise, a generic alert with the error’s message is posted.

This takes care of the production code, but you also need to update the
MockPedometer.

Open MockPedometer.swift and replace start() with the following:

var error: Error?

func start(completion: @escaping (Error?) -> Void) {


started = true
DispatchQueue.global(qos: .default).async {
completion(self.error)
}
}

static let notAuthorizedError =


NSError(
domain: CMErrorDomain,
code: Int(CMErrorMotionActivityNotAuthorized.rawValue),
userInfo: nil)

This update will call the completion, passing its error property. For convenience,
the static notAuthorizedError creates an error object that matches what is
returned by Core Motion when unauthorized. This is what you used in
testAppModel_whenDeniedAuthAfterStart_generatesAlert.

Build and test again, and your tests should pass.

raywenderlich.com 125
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Getting actual data


It’s time move on to handling data updates. The incoming data is the most important
part of the app, and it’s crucial to have it properly mocked. The actual step and
distance count are provided by CMPedometer through the aptly named
CMPedometerData object. This too should be abstracted between the app and Core
Motion.

Open Pedometer.swift and add the following protocol:

protocol PedometerData {
var steps: Int { get }
var distanceTravelled: Double { get }
}

This adds an abstraction around CMPedometerData so that the step and distance data
can be mocked. Do that by creating a new .swift file in the Mocks group of the test
target: MockData.swift and replacing its contents with the following:

@testable import FitNess

struct MockData: PedometerData {


let steps: Int
let distanceTravelled: Double
}

With this in place, open AppModelTests.swift and add the following test at the end
of the class definition:

func testModel_whenPedometerUpdates_updatesDataModel() {
// given
givenInProgress()
let data = MockData(steps: 100, distanceTravelled: 10)

// when
mockPedometer.sendData(data)

// then
XCTAssertEqual(sut.dataModel.steps, 100)
XCTAssertEqual(sut.dataModel.distance, 10)
}

The test verifies that the supplied data is applied to the data model. This requires an
update to MockPedometer to pass the data. First, think about how that data will
eventually be passed to AppModel.

raywenderlich.com 126
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Open Pedometer.swift. In the Pedometer protocol, change the signature of


start(completion:) to the following:

func start(
dataUpdates: @escaping (PedometerData?, Error?) -> Void,
eventUpdates: @escaping (Error?) -> Void)

The dataUpdates block will provide a means of returning PedometerData from the
pedometer. eventUpdates will return events, as the old completion block did.

In MockPedometer, create two new variables to hold these callback blocks:

var updateBlock: ((Error?) -> Void)?


var dataBlock: ((PedometerData?, Error?) -> Void)?

Next, replace start(completion:) with the following:

func start(
dataUpdates: @escaping (PedometerData?, Error?) -> Void,
eventUpdates: @escaping (Error?) -> Void
) {
started = true
updateBlock = eventUpdates
dataBlock = dataUpdates
DispatchQueue.global(qos: .default).async {
self.updateBlock?(self.error)
}
}

func sendData(_ data: PedometerData?) {


dataBlock?(data, error)
}

The two blocks are saved for later use, but the updateBlock is still called as part of
this method, as completion was previously. You won’t have to update any previous
tests for this one, as the behavior is the same. Also added is sendData(_:), which is
used by the test to call the dataBlock with the mock data.

raywenderlich.com 127
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

You also need to update the CMPedometer extension for this new logic. Open
CMPedometer+Pedometer.swift and change start(completion:) to the
following:

func start(
dataUpdates: @escaping (PedometerData?, Error?) -> Void,
eventUpdates: @escaping (Error?) -> Void) {

startEventUpdates { _, error in
eventUpdates(error)
}

startUpdates(from: Date()) { data, error in


dataUpdates(data, error)
}
}

This preserves the previous startEventUpdates behavior, plus adds a new call to
startUpdates to forward the data updates.

You also need to wrap CMPedometerData with the new PedometerData protocol. Add
the following extension to bottom of the file:

extension CMPedometerData: PedometerData {


var steps: Int {
return numberOfSteps.intValue
}

var distanceTravelled: Double {


return distance?.doubleValue ?? 0
}
}

This forwards the CMPedometerData values as PedometerData variables.

Finally, open AppModel.swift, and replace startPedometer() with the following:

func startPedometer() {
pedometer.start(
dataUpdates: handleData,
eventUpdates: handleEvents)
}

func handleData(data: PedometerData?, error: Error?) {


if let data = data {
dataModel.steps += data.steps
dataModel.distance += data.distanceTravelled
}
}

raywenderlich.com 128
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

func handleEvents(error: Error?) {


if let error = error {
let alert = error.is(CMErrorMotionActivityNotAuthorized)
? .notAuthorized
: Alert(error.localizedDescription)
AlertCenter.instance.postAlert(alert: alert)
}
}

This moves the previous event handling to its own method and creates a new one to
update dataModel when there is new data. You’ll notice that data update errors are
not handled here. That’s left as a Challenge for you after this chapter is complete!

Build and test, and watch that green grow!

Making a functional fake


At this point it sure would be nice to see the app in action. The unit tests are useful
for verifying logic but are bad at verifying you’re building a good user experience. One
way to do that is to build and run on a device, but that will require you to walk
around to complete the goal. That’s very time and calorie consuming. There has got
to be a better way!

Enter the fake pedometer: You’ve already done the work to abstract the app from a
real CMPedometer, so it’s straightforward to build a fake pedometer that speeds up
time or makes up movement.

Create a new .swift file in the pedometer group: SimulatorPedometer.swift.


Replace its contents with the following:

import Foundation

class SimulatorPedometer: Pedometer {


struct Data: PedometerData {
let steps: Int
let distanceTravelled: Double
}

var pedometerAvailable: Bool = true


var permissionDeclined: Bool = false

var timer: Timer?


var distance = 0.0

var updateBlock: ((Error?) -> Void)?


var dataBlock: ((PedometerData?, Error?) -> Void)?

raywenderlich.com 129
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

func start(
dataUpdates: @escaping (PedometerData?, Error?) -> Void,
eventUpdates: @escaping (Error?) -> Void
) {
updateBlock = eventUpdates
dataBlock = dataUpdates

timer = Timer(
timeInterval: 1,
repeats: true
) { _ in
self.distance += 1
print("updated distance: \(self.distance)")
let data = Data(
steps: 10,
distanceTravelled: self.distance)
self.dataBlock?(data, nil)
}
RunLoop.main.add(timer!, forMode: RunLoop.Mode.default)
updateBlock?(nil)
}

func stop() {
timer?.invalidate()
updateBlock?(nil)
updateBlock = nil
dataBlock = nil
}
}

This giant block of code implements the Pedometer and PedometerData protocols. It
sets up a Timer object that, once start is called, adds ten steps every second. Each
time it updates, it calls dataBlock with the new data.

You’ve also added a stop method that stops the timer and cleans up. This will be
used when you add the ability to pause the pedometer by tapping the Pause button.

To use the simulated pedometer in the app, open AppModel.swift, and add the
following static var:

static var pedometerFactory: (() -> Pedometer) = {


#if targetEnvironment(simulator)
return SimulatorPedometer()
#else
return CMPedometer()
#endif
}

This method creates either a SimulatorPedometer() or a CMPedometer()


depending on the app’s target environment.

raywenderlich.com 130
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Next, replace init with the following:

init(pedometer: Pedometer = pedometerFactory()) {


self.pedometer = pedometer
}

Now build and run in Simulator. Tap the settings cog in the lower-right and enter a
goal of 100 steps.

Tap Start, and you’ll see alert notifications coming in!

Wiring up the chase view


Looking at the app now, that white box in the middle is a little disappointing. This is
the chase view (it illustrates Nessie’s chase of the user), and hasn’t yet been wired
up.

In order to test that it will accurately reflect the user’s state, you can use a partial
mock. By partially mocking the chase view, you can add a little extra test
functionality without interrupting its main logic. This is instead of a full mock,
which replaces all functionality.

Create a new file in the Mocks group called ChaseViewPartialMock.swift and


replace its contents with the following:

@testable import FitNess

raywenderlich.com 131
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

class ChaseViewPartialMock: ChaseView {


var updateStateCalled = false
var lastRunner: Double?
var lastNessie: Double?

override func updateState(runner: Double, nessie: Double) {


updateStateCalled = true
lastRunner = runner
lastNessie = nessie
super.updateState(runner: runner, nessie: nessie)
}
}

This partial mock overrides updateState(runner:nessie:) so that the values sent


to it can be recorded and verified in tests. updateStateCalled can be used by tests
to track that the method has been called — a common mock validation.

This class is used by StepCountController.

First open StepCountControllerTests.swift and add the following variable:

var mockChaseView: ChaseViewPartialMock!

Next, add the following lines to the bottom of setUpWithError:

mockChaseView = ChaseViewPartialMock()
sut.chaseView = mockChaseView

Finally, add a test that verifies that the view gets updated:

func testChaseView_whenDataSent_isUpdated() {
// given
givenInProgress()

// when
let data = MockData(steps: 500, distanceTravelled: 10)
(AppModel.instance.pedometer as! MockPedometer).sendData(data)

// then
XCTAssertTrue(mockChaseView.updateStateCalled)
XCTAssertEqual(mockChaseView.lastRunner, 0.5)
}

raywenderlich.com 132
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

This uses the mocked pedometer to send data and verifies the state on the partial
mock chase view. The value for Nessie’s position isn’t checked since the code for
Nessie isn’t part of the project yet.

Build and test, and you’ll see neither assert passes, because the chase view isn’t yet
being updated.

Open StepCountController.swift, and add the following to viewDidLoad() to kick


off this update:

NotificationCenter.default
.addObserver(
forName: DataModel.UpdateNotification,
object: nil,
queue: nil) { _ in
self.updateUI()
}

This listens for data model updates and calls updateUI when there is a data update.

updateUI calls updateChaseView, which needs to calculate the location of Nessie


and the runner, then update them in the view. Replace updateChaseView with with
the following:

private func updateChaseView() {


chaseView.state = AppModel.instance.appState
let dataModel = AppModel.instance.dataModel
let runner =
Double(dataModel.steps) / Double(dataModel.goal ?? 10_000)
let nessie = dataModel.nessie.distance > 0 ?
dataModel.distance / dataModel.nessie.distance : 0
chaseView.updateState(runner: runner, nessie: nessie)
}

This gathers the distance of the user and Nessie from the data model, computes a
percent completion, and presents it to the chase view so that the avatars can be
placed accordingly.

raywenderlich.com 133
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Build and test to see the test pass! Build and run to see the view in action:

Time dependencies
The final major piece missing is Nessie. She should be chasing after the user while
the app is in progress. Her progress will be measured at a constant velocity.
Measuring something over time? Sounds like a Timer is the answer.

Timers are notoriously hard to test: They require using expectations along with
having a potentially long wait. There are few common solutions:

1. During tests, use a very short timer (e.g., one millisecond instead of one second).

2. Swap the timer for a mock that executes the callback immediately.

3. Use the callback directly, and save the timing for app or user-acceptance testing.

Any of these are reasonable solutions, but you’re going to go with option #3. In
NessieTests.swift, add this test:

func testNessie_whenUpdated_incrementsDistance() {
// when
sut.incrementDistance()

// then
XCTAssertEqual(sut.distance, sut.velocity)
}

raywenderlich.com 134
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

This calls incrementDistance directly, just as the Timer callback does in the Nessie
class. It asserts that after distance increments it is equal to the velocity.

The test doesn’t yet pass, because incrementDistance is stubbed out. Open
Nessie.swift, and add the following line to incrementDistance():

distance += velocity

The distance now increments, and the test will pass.

Challenge
You’ve reached the end of the chapter, but not the end of the app. You should be able
to take the testing tools you’ve learned and finish the app. Your challenge is to add
the following tests and features to complete the app:

• Complete the Pause functionality to be able to pause and resume the pedometer.

• Wire up Nessie to app state so it can start, pause and reset appropriately. You’ll
also have to give the user a little bit of a head start since both the user and Nessie
will start at 0.

• Complete the handling of data errors from the pedometer (use the Alert Center).

Key points
• Test doubles let you test code in isolation from other systems, especially those
that are part of system SDKs, rely on networking or timers.

• Mocks let you swap in a test implementation of a class, and partial mocks let you
just substitute part of a class.

• Fakes let you supply data for testing or use in Simulator.

raywenderlich.com 135
iOS Test-Driven Development by Tutorials Chapter 6: Dependency Injection & Mocks

Where to go from here?


That’s it. Over the past few chapters, you’ve built an an app from the ground up
following TDD principles.

This chapter covered using mocks to separate the test subjects from external code
and events. This just scratches the surface of what’s possible. The next section will
be all about using external services like network requests.

If you want to learn more about the use and history of doubles, read this excellent
Martin Fowler article, “Mocks Aren’t Stubs”: https://martinfowler.com/articles/
mocksArentStubs.html.

raywenderlich.com 136
Section III: TDD With
Networking

This section will teach you test-driven development with networking.

You’ll get hands-on experience creating a puppy-buying app that interacts with a
backend service. You’ll learn how to do TDD for RESTful networking, using network
clients and downloading images throughout this section.

raywenderlich.com 137
7 Chapter 7: Introducing
Dog Patch
By Joshua Greene

You’ve learned the basics of TDD, and you should be starting to feel comfortable
with it. However, you haven’t learned how to do TDD for a very critical part of most
apps: networking!

Over the next several chapters, you’ll learn the ins-and-outs of writing networking
code in a test-driven fashion. The goal of this chapter is to introduce you to this
section’s sample project and highlight what work remains to be completed.

raywenderlich.com 138
iOS Test-Driven Development by Tutorials Chapter 7: Introducing Dog Patch

Getting started
You’ll complete a puppy-adoption app called Dog Patch throughout this section.
This app connects dog lovers with kind, professional breeders to find the puppy of
their dreams.

Let’s go over what needs to be done to make this possible.

Networking client
In Chapter 8, you’ll learn how to start TDD for RESTful networking. You’ll first
explore the starter project and find that ListingsViewController always shows an
error:

This is because the app isn’t actually doing any networking yet! You’ll create a
networking client and make a GET request to fetch Dog models from a remote server
as the first steps to fix this.

raywenderlich.com 139
iOS Test-Driven Development by Tutorials Chapter 7: Introducing Dog Patch

Using a networking client


In chapter 9, you’ll follow TDD to use your networking client in a view controller.
Ultimately, your app will be able to display networking results to the user, except for
images:

Specifically, you’ll update ListingsViewController to use DogPatchClient, the


networking client that you created in the last chapter.

Image client
In chapter 10, you’ll create an image client and update ListingsViewController to
use it to display images:

raywenderlich.com 140
iOS Test-Driven Development by Tutorials Chapter 7: Introducing Dog Patch

Understanding Dog Patch’s architecture


You’ll use Model-View-Controller-Networking (MVC-N) for this app’s
architecture. If you’ve done any work in iOS before, you’re very likely familiar with
the Model-View-Controller (MVC) architecture, wherein you separate objects into
three types. These are aptly named models, views and controllers, of course.

MVC-N is a spin-off architecture of MVC. Instead of just three types, however, it


separates out a fourth type for networking.

Especially for networking-heavy apps like Dog Patch, it makes sense to separate
networking into its own type. If you didn’t do this, where would the networking code
go, after all? In pure MVC-architecture apps, developers tend to lump networking
into each view controller.

The problem here is that a lot of networking code is interrelated. For example, URL
and content serialization, authentication headers and more require exactly the same
logic. If networking code is directly in each view controller, you quickly wind up with
a lot of duplication. This quickly becomes an unmanageable mess as a result.

Fortunately, MVC-N allows you to avoid this issue altogether by putting your
networking code into a networking client. This client is then passed into whatever
view controllers need it, and this effectively eliminates the duplication across view
controllers.

It’s OK if you haven’t heard of MVC-N before. You’ll learn all about it over the course
of the next few chapters!

Where to go from here?


This chapter introduced Dog Patch and what you’ll be doing over the next few
chapters. Continue onto the next chapter to dive into the code!

raywenderlich.com 141
8 Chapter 8: RESTful
Networking
By Joshua Greene

In this chapter, you’ll learn how to TDD a RESTful networking client. Specifically,
you’ll:

• Set up the networking client.

• Ensure the correct endpoint is called.

• Handle networking errors, valid responses and invalid responses.

• Dispatch results to a response queue.

Get excited! TDD networking awesomeness is coming your way.

Getting started
Navigate to the starter directory for this chapter. You’ll find it has a DogPatch
subdirectory containing DogPatch.xcodeproj. Open this project file in Xcode and
take a look.

You’ll see a few files waiting for you. The important ones for this chapter are:

• Controllers/ListingsViewController.swift displays the fetched Dogs or Error.

• Models/Dog.swift is the model that represents each pup.

• Networking is an empty folder for now. You’ll add the networking client and
related types here.

raywenderlich.com 142
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Build and run the app, and the following error-message screen will greet you:

If you pull down to refresh, the activity indicator will animate but won’t ever finish.

Open ListingsViewController.swift. You’ll see


tableView(_:numberOfRowsInSection:) returns the max of viewModels.count or
one, if it isn’t currently refreshing.

Similarly, tableView(_:cellForRowAt:) checks if viewModels.count is greater


than zero, which will always be false because the app isn’t setting viewModels right
now. Rather, you need to create these from a network response.

However, there’s a comment for // TODO: Write this within refreshData(), so


the app isn’t making any network calls…

Your job is now clear! You need to write the logic to make networking calls. While
you could make a one-off network call directly within ListingsViewController,
this view controller would quickly become very large.

A better option is to create a separate networking client that handles all of the
networking logic, which happens to be the focus of this chapter!

raywenderlich.com 143
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Setting up the networking client


Before you write any production code, you first need to write a failing test.

Within DogPatchTests/Cases/Networking, create a new Swift File called


DogPatchClientTests.swift. Replace its contents with the following, ignoring the
compiler error for now:

@testable import DogPatch


import XCTest

class DogPatchClientTests: XCTestCase {


var sut: DogPatchClient!
}

Here, you create a new test class for DogPatchClientTests with a single property
for sut of type DogPatchClient. However, since you haven’t created
DogPatchClient, this code doesn’t compile. Compiler errors count as test failures,
so you can now write production code.

Within DogPatch/Networking, create a new Swift File called


DogPatchClient.swift and replace its contents with the following:

import Foundation

class DogPatchClient {

Here, you declare a new class for DogPatchClient and thereby resolve the compiler
error. There’s nothing to refactor, so you simply move on to your first test.

Open DogPatchClientTests.swift and add the following below the declaration for
sut, again ignoring the compiler error:

func test_init_sets_baseURL() {
// given
let baseURL = URL(string: "https://example.com/api/v1/")!

// when
sut = DogPatchClient(baseURL: baseURL)
}

Ultimately, you want to test that the baseURL passed into the initializer matches
sut.baseURL. However, you haven’t created this initializer, so this doesn’t compile.

raywenderlich.com 144
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

To fix this, add the following to DogPatchClient:

let baseURL = URL(string: "https://example.com/")!

init(baseURL: URL) {

You declare baseURL, set it to an arbitrary value for now and then create
init(baseURL:), which is enough to get the test to compile. But you haven’t
asserted anything yet.

In DogPatchClientTests, add the following to the end of the test method:

// then
XCTAssertEqual(sut.baseURL, baseURL)

You assert sut.baseURL equals baseURL passed to the initializer. Build and run the
unit tests. As expected, this test fails.

To get this to pass, replace the line for let baseURL = within DogPatchClient with:

let baseURL: URL

Then add the following to init(baseURL:):

self.baseURL = baseURL

You set baseURL from the passed-in argument into the initializer. Build and run your
tests, and this now passes. There isn’t anything to refactor, so simply continue.

You also need a property for URLSession, which you’ll use to make the networking
calls.

Add the following test right after the previous one, again ignoring the compiler
error:

func test_init_sets_session() {
// given
let baseURL = URL(string: "https://example.com/api/v1/")!
let session = URLSession.shared

// when
sut = DogPatchClient(baseURL: baseURL, session: session)
}

The purpose of this test is to expand the initializer to accept a session argument.

raywenderlich.com 145
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Like before, you haven’t declared session on DogPatchClient, so this doesn’t


compile. To fix it, add the following property after baseURL on DogPatchClient:

let session: URLSession = URLSession(configuration: .default)

Next, update the method signature for init(baseURL:) to:

init(baseURL: URL, session: URLSession)

This lets test_init_sets_session() compile, but it breaks


test_init_sets_baseURL(). To fix this, add this line right below let baseURL
within test_init_sets_baseURL():

let session = URLSession.shared

Then update the line for sut = to:

sut = DogPatchClient(baseURL: baseURL, session: session)

Your tests now compile again, but you haven’t added an assertion to
test_init_sets_session(). Add the following to the end of that test method:

// then
XCTAssertEqual(sut.session, session)

You assert that sut.session and session are equal.

Build and run your tests. As expected, this test fails. To make it pass, change the
property declaration for session on DogPatchClient to:

let session: URLSession

Then add this line to the end of the initializer:

self.session = session

Build and run the tests, and see they both now pass. This time, you do have some
refactoring to do.

The first several lines within test_init_sets_baseURL() and


test_init_sets_session() are exactly the same. To fix this, first add the following
properties at the top of the class, right before var sut:

var baseURL: URL!


var session: URLSession!

raywenderlich.com 146
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Next, add these two methods right after the properties:

override func setUp() {


super.setUp()
baseURL = URL(string: "https://example.com/api/v1/")!
session = URLSession.shared
sut = DogPatchClient(baseURL: baseURL, session: session)
}

override func tearDown() {


baseURL = nil
session = nil
sut = nil
super.tearDown()
}

You set each of the properties within setUp and nil each of the properties within
tearDown, which helps you eliminate the duplication in your tests.

Replace the entire contents of test_init_sets_baseURL() with:

XCTAssertEqual(sut.baseURL, baseURL)

Then replace the contents of test_init_sets_session() with:

XCTAssertEqual(sut.session, session)

Build and run the tests. You’ll see they all pass.

Excellent job! You declared two properties!

OK, maybe it’s not that exciting… However, these properties are essential to making
networking calls, and now you can write that code!

raywenderlich.com 147
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

TDDing the networking call


You need to make a GET request to fetch a list of Dog objects from the server. You’ll
break this down into several smaller tasks:

1. Mocking URLSesssion.

2. Calling the right URL.

3. Handling error responses.

4. Deserializing models on success.

5. Handling invalid responses.

Mocking URLSession
To keep your tests fast and repeatable, don’t make real networking calls in them.
Instead of using a real URLSession, you’ll create a MockURLSession that will let you
verify behavior but won’t make network calls. You’ll pass this into the initializer for
DogPatchClient and use it like you’d use a real URLSession.

You might be tempted to create this by subclassing and overriding URLSession.


However, if you try this, you’ll find its init has been deprecated, and its other
initializers are marked public instead of open. Consequently, you can’t effectively
subclass URLSession!

Fortunately, there’s another solution. Instead of using URLSession directly, you’ll


create a URLSessionProtocol and update DogPatchClient to use it. In production
code, you’ll make URLSession conform to URLSessionProtocol and pass this into
DogPatchClient. In unit tests, you’ll make MockURLSession conform and pass it
instead.

OK, you’ve got the theory down! Time to write the code.

Creating the session protocols


Within DogPatch/Networking, create a new Swift File called
URLSessionProtocol.swift and replace its contents with:

import Foundation

protocol URLSessionProtocol: AnyObject {

func makeDataTask(

raywenderlich.com 148
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

with url: URL,


completionHandler:
@escaping (Data?, URLResponse?, Error?) -> Void)
-> URLSessionTaskProtocol
}

protocol URLSessionTaskProtocol: AnyObject {


func resume()
}

You declare URLSessionProtocol with a single required method,


makeDataTask(with:completionHandler:), which returns a
URLSessionTaskProtocol instead of a real URLSessionTask directly. This lets you
mock URLSessionTask and verify its behavior. URLSessionTaskProtocol also has a
single required method, resume(), to start the networking task.

But wait, aren’t you missing unit tests? Nope! Protocols don’t have concrete
behavior, so there’s nothing to test.

Conforming to the session protocols


You next need to make URLSession conform to URLSessionProtocol, and
URLSessionTask conform to URLSessionTaskProtocol. Because
URLSessionProtocol uses URLSessionTaskProtocol, start by making
URLSessionTask conform first.

Since URLSessionTask does require concrete implementation code, you’ll need to


write tests for it.

Under DogPatchClient/Cases/Networking, create a new Swift File called


URLSessionProtocolTests.swift and replace its contents with:

@testable import DogPatch


import XCTest

class URLSessionProtocolTests: XCTestCase {


var session: URLSession!

override func setUp() {


super.setUp()
session = URLSession(configuration: .default)
}

override func tearDown() {


session = nil
super.tearDown()
}

raywenderlich.com 149
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

func test_URLSessionTask_conformsTo_URLSessionTaskProtocol() {
// given
let url = URL(string: "https://example.com")!

// when
let task = session.dataTask(with: url)

// then
XCTAssertTrue((task as AnyObject) is URLSessionTaskProtocol)
}
}

You create a new test case for URLSessionProtocolTests with a single property,
session, and a single unit test that verifies URLSessionTask conforms to
URLSessionTaskProtocol. Since URLSessionTask doesn’t have any non-deprecated
initializers, you can’t create it directly. Instead, you call session.dataTask(with:)
to create one.

Build and run the tests, and you’ll see this test fails as intended. To make it pass, add
the following to the end of URLSessionProtocol.swift:

extension URLSessionTask: URLSessionTaskProtocol { }

You extend URLSessionTask to make it conform to URLSessionTaskProtocol. Since


URLSessionTask already implements resume(), you don’t need to add it here.

Build and rerun the tests, and see them pass. There’s nothing to refactor, so just
continue.

Technical note: If you’re familiar with the ins and outs of URLSession, you
know that dataTask(with:) returns a URLSessionDataTask, and
URLSessionTask is its superclass.

By making URLSessionTask conform to URLSessionTaskProtocol, instead of


URLSessionDataTask, you also get conformance for all subclasses, including
both public and internal types that URLSession creates and returns.
Consequently, it’s a more flexible and better design.

How could you have figured this out by yourself? By using TDD and trial-and-
error! However, this process isn’t essential to the core concepts in this chapter,
so I omitted it for brevity’s sake.

raywenderlich.com 150
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Add this test after the previous one in URLSessionProtocolTests:

func test_URLSession_conformsTo_URLSessionProtocol() {
XCTAssertTrue((session as AnyObject) is URLSessionProtocol)
}

You validate that session, an instance of URLSession, conforms to


URLSessionProtocol. Build and run tests, and you’ll see this fails as expected.

To make it pass, add the following to the end of URLSessionProtocol.swift:

extension URLSession: URLSessionProtocol {

func makeDataTask(
with url: URL,
completionHandler:
@escaping (Data?, URLResponse?, Error?) -> Void)
-> URLSessionTaskProtocol {

let url = URL(string: "http://fake.example.com")!


return dataTask(with: url,
completionHandler: { _, _, _ in } )
}
}

You extend URLSession to conform to URLSessionProtocol and implement


makeDataTask by calling dataTask with dummy values. Build and run the tests, and
you’ll see they all pass. There’s still nothing to refactor, so continue.

Next, you need to verify makeDataTask calls dataTask with the passed-in url. Add
this test after the last one:

func test_URLSession_makeDataTask_createsTaskWithPassedInURL() {
// given
let url = URL(string: "https://example.com")!

// when
let task = session.makeDataTask(
with: url,
completionHandler: { _, _, _ in })
as! URLSessionTask

// then
XCTAssertEqual(task.originalRequest?.url, url)
}

You assert the passed-in url matches the url on task.originalRequest. Build and
run the tests to confirm this indeed fails.

raywenderlich.com 151
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

To make it pass, replace the contents of makeDataTask with:

return dataTask(with: url, completionHandler: { _, _, _ in } )

Here, you update the return statement to use the passed-in url. Build and run the
tests. The last one will now pass.

Now there’s duplicated code in URLSessionProtocolTests: You create the same url
in two different tests. To fix this, declare this new property, right below session:

var url: URL!

In setUp(), add this line right after setting the session, to create the url:

url = URL(string: "https://example.com")!

Then in tearDown(), add this right after setting session, to nil out the url:

url = nil

Delete the // given and let url lines from both


test_URLSessionTask_conformsTo_URLSessionTaskProtocol and
test_URLSession_makeDataTask_createsTaskWithPassedInURL to get rid of the
duplication. Build and run the tests, and see that they all continue to pass.

Next, add this test right after the last one:

func
test_URLSession_makeDataTask_createsTaskWithPassedInCompletion()
{
// given
let expectation =
expectation(description: "Completion should be called")

// when
let task = session.makeDataTask(
with: url,
completionHandler: { _, _, _ in expectation.fulfill() })
as! URLSessionTask
task.cancel()

// then
waitForExpectations(timeout: 0.2, handler: nil)
}

raywenderlich.com 152
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

This test verifies the completionHandler is set correctly. As such, you create an
expectation and fulfill it when the completionHandler is called. By calling
task.cancel(), you cause the completionHandler to execute. You verify the
expectation is fulfilled via waitForExpecations.

Build and run the tests, and you’ll see this test fails. To make it pass, update the
contents of makeDataTask with:

return dataTask(with: url,


completionHandler: completionHandler)

You update the return statement to use the passed-in completionHandler. Build
and run the tests, and see them all pass.

Creating and using the session mocks


Next, you need to create test types for MockURLSession and MockURLSessionTask.

Under Test Types/Mocks, create a new Swift File called MockURLSession.swift.


Replace its contents with:

@testable import DogPatch


import Foundation

// 1
class MockURLSession: URLSessionProtocol {

func makeDataTask(
with url: URL,
completionHandler: @escaping (Data?, URLResponse?, Error?)
-> Void)
-> URLSessionTaskProtocol {
return MockURLSessionTask(
completionHandler: completionHandler,
url: url)
}
}

// 2
class MockURLSessionTask: URLSessionTaskProtocol {

var completionHandler: (Data?, URLResponse?, Error?) -> Void


var url: URL

init(completionHandler:
@escaping (Data?, URLResponse?, Error?) -> Void,
url: URL) {
self.completionHandler = completionHandler
self.url = url

raywenderlich.com 153
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

// 3
func resume() {

}
}

Here’s what you did:

1. You declare MockURLSession as conforming to URLSessionProtocol and create


a MockURLSessionTask from makeDataTask.

2. You create MockURLSessionTask as conforming to URLSessionTaskProtocol,


declare properties for url and completionHandler and set these within its
initializer. This lets you use these in your tests.

3. You implement resume as an empty method for now.

Instead of passing a real URLSession into DogPatchClient, you’ll pass an instance


of MockURLSession.

To make it clear this is a mock, back in DogPatchClientTests.swift, right-click the


session property, select Refactor -> Rename and change its name to mockSession.
Next, replace the var mockSession line with the following, ignoring the compiler
errors for now:

var mockSession: MockURLSession!

This code changes the mockSession variables type to MockURLSession, but it breaks
the tests in a few places. First, you need to update where you set this property within
setUp().

Replace the mockSession = line with:

mockSession = MockURLSession()

Next, within DogPatchClient.swift, you need to update the type of session. First,
replace the let session line with:

let session: URLSessionProtocol

Next, replace the init(baseURL: URL, session: URLSession) declaration with:

init(baseURL: URL, session: URLSessionProtocol)

raywenderlich.com 154
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Back in DogPatchClientTests.swift, replace the contents of


test_init_sets_session with:

XCTAssertTrue(sut.session === mockSession)

You use the identity operator === to assert that sut.session and mockSession are
the same instance.

These changes resolve all of the compiler errors. Build and run the tests and verify
they all pass.

Calling the right URL


Now, use MockURLSession to validate behavior within your tests!

Still in DogPatchClientTests, add the following test after the last one, ignoring the
compiler error:

func test_getDogs_callsExpectedURL() {
// given
let getDogsURL = URL(string: "dogs", relativeTo: baseURL)!

// when
let mockTask = sut.getDogs() { _, _ in }
as! MockURLSessionTask
}

This test verifies getDogs calls a specific URL. However, it doesn’t compile because
you haven’t declared getDogs yet in the production code. Add this after the other
methods in DogPatchClient:

func getDogs(completion: @escaping


([Dog]?, Error?) -> Void) -> URLSessionTaskProtocol {
return session.makeDataTask(with: baseURL) { _, _, _ in }
}

This method calls session.makeDataTask(with:completionHandler:) using


baseURL and an empty closure as dummy values. You need an assertion to verify the
right URL is called. Open DogPatchClientTests.swift and add this to the end of
test_getDogs_callsExpectedURL():

// then
XCTAssertEqual(mockTask.url, getDogsURL)

raywenderlich.com 155
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Build and run the tests, and you’ll see this fails. You can now write the code to call
the correct URL.

Open DogPatchClient.swift, and replace the contents of getDogs(completion:)


with the following:

let url = URL(string: "dogs", relativeTo: baseURL)!


return session.makeDataTask(with: url) { _, _, _ in }

Build and run the tests, and see them all pass.

URLSession doesn’t start a networking task after it’s created. Instead, you must call
resume on the task. You need a test that verifies that you called this.

Before you write this test, in MockURLSession.swift, replace resume() on


MockURLSessionTask with the following:

var calledResume = false


func resume() {
calledResume = true
}

You declare a new property for calledResume, which defaults to false, and set it to
true within resume(). Now, you can write a test that uses this.

Open DogPatchClientTests.swift, and add the following after the last test:

func test_getDogs_callsResumeOnTask() {
// when
let mockTask = sut.getDogs() { _, _ in }
as! MockURLSessionTask

// then
XCTAssertTrue(mockTask.calledResume)
}

Build and run, and you’ll see this test fails as expected. To make it pass, in
DogPatchClientTests.swift, replace the return line within getDogs(completion:)
on DogPatchClient with:

let task = session.makeDataTask(with: url) {


data, response, error in
}
task.resume()
return task

Build and run your tests. You’ll see they pass now. There’s nothing to refactor so you
can continue!

raywenderlich.com 156
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Handling error responses


Next, you need to handle error responses. Two scenarios indicate an error occurred:

1. The server returns an HTTP status code besides 200. This endpoint always
returns 200 on success. If another status code returns, the request failed.

2. The request may never reach the server, may timeout or another error condition
may happen at the networking layer. In this case, the error will be set.

Start by writing a test that checks for the first scenario. Add the following test after
the last one:

func test_getDogs_givenResponseStatusCode500_callsCompletion() {
// given
let getDogsURL = URL(string: "dogs", relativeTo: baseURL)!
let response = HTTPURLResponse(url: getDogsURL,
statusCode: 500,
httpVersion: nil,
headerFields: nil)

// when
var calledCompletion = false
var receivedDogs: [Dog]? = nil
var receivedError: Error? = nil

let mockTask = sut.getDogs() { dogs, error in


calledCompletion = true
receivedDogs = dogs
receivedError = error
} as! MockURLSessionTask

mockTask.completionHandler(nil, response, nil)

// then
XCTAssertTrue(calledCompletion)
XCTAssertNil(receivedDogs)
XCTAssertNil(receivedError)
}

raywenderlich.com 157
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Here’s what you did:

• Within given, you create response using getDogsURL and an HTTP status of 500
indicating a failure.

• Within when, you create variables to hold whether the completion closure was
called and the return values. Then you call the completionHandler on the
mockTask.

• Within then, you assert the completion handler was called, and the received
values for dogs and error are nil.

Build and run your tests. As expected, this test will fail because the completion
handler isn’t called.

To fix this, add the following inside the closure for session.dataTask(with: url)
within getDogs on DogPatchClient:

guard let response = response as? HTTPURLResponse,


response.statusCode == 200 else {
completion(nil, error)
return
}

This guard statement checks that the status code is the expected 200 result and calls
the completion handler if it isn’t.

Build and run your tests. Your test now passes. Do you see anything to refactor? Yep,
getDogsURL is precisely the same in two tests.

To remove this duplication, add the following computed property right after the sut
declaration in DogPatchClientTests:

var getDogsURL: URL {


return URL(string: "dogs", relativeTo: baseURL)!
}

Then delete the entire given section from test_getDogs_callsExpectedURL, and


delete the let getDogsURL line from
test_getDogs_givenResponseStatusCode500_callsCompletion.

Build and run your tests. They’ll continue to pass.

raywenderlich.com 158
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Next, you’ll deal with the other error scenario, when an error returns. Add the
following test case to check for this:

func test_getDogs_givenError_callsCompletionWithError() throws {


// given
let response = HTTPURLResponse(url: getDogsURL,
statusCode: 200,
httpVersion: nil,
headerFields: nil)
let expectedError = NSError(domain: "com.DogPatchTests",
code: 42)

// when
var calledCompletion = false
var receivedDogs: [Dog]? = nil
var receivedError: Error? = nil

let mockTask = sut.getDogs() { dogs, error in


calledCompletion = true
receivedDogs = dogs
receivedError = error as NSError?
} as! MockURLSessionTask

mockTask.completionHandler(nil, response, expectedError)

// then
XCTAssertTrue(calledCompletion)
XCTAssertNil(receivedDogs)

let actualError = try XCTUnwrap(receivedError as NSError?)


XCTAssertEqual(actualError, expectedError)
}

Here’s what you did:

• Within given, you create a response with a statusCode of 200 and an


expectedError. It’s unlikely that you’ll have a “success” response code of 200 and
also an error. However, perhaps the server is behaving incorrectly, or you’ve run
into an edge case of some sort in the real world. Hey, server developers aren’t
perfect either. Pragmatically though, this ensures your previous guard on the
statusCode doesn’t trigger in this case.

• Within when, you set variables to check whether the completion was called and
what values were received. Then, you call the completionHandler on the
mockTask with the response and expectedError from before.

• Within then, you assert the completion is called, the received dogs are nil and the
error matches what you expect.

raywenderlich.com 159
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Build and run your tests. The assertions for calledCompletion and unwrapping
receivedError fail, which makes sense since you haven’t written this code yet.

You can also temporarily change the assignment of receivedDogs to an empty array
to prove that XCTAssertNil(receivedDogs) fails, but be sure to set this property
back to nil before continuing.

To make all the asserts pass, replace the entire guard line within getDogs on
DogPatchClient with:

guard let response = response as? HTTPURLResponse,


response.statusCode == 200,
error == nil else {

Build and run your tests. They’ll all pass now. However, now there’s a lot of code
duplication between this test and the previous one.

To fix this, pull out a helper method for the common code. Add the following method
right after tearDown, since it’s called from several tests:

func whenGetDogs(
data: Data? = nil,
statusCode: Int = 200,
error: Error? = nil) ->
(calledCompletion: Bool, dogs: [Dog]?, error: Error?) {

let response = HTTPURLResponse(url: getDogsURL,


statusCode: statusCode,
httpVersion: nil,
headerFields: nil)

var calledCompletion = false


var receivedDogs: [Dog]? = nil
var receivedError: Error? = nil

let mockTask = sut.getDogs() { dogs, error in


calledCompletion = true
receivedDogs = dogs
receivedError = error as NSError?
} as! MockURLSessionTask

mockTask.completionHandler(data, response, error)


return (calledCompletion, receivedDogs, receivedError)
}

raywenderlich.com 160
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Here’s how this works:

• This method accepts inputs for data, statusCode and error. As a convenience,
you also provide appropriate default values for each. It returns a tuple with values
for calledCompletion, dogs and error.

• It creates the response using getDogsURL and the passed-in statusCode.

• Then, it creates local variables, calls getDogs on sut and calls the
completionHandler on mockTask, as the previous tests did.

• Finally, the method returns the tuple created from the local variables for
calledCompletion, receivedDogs and receivedError.

You can use this method to remove the duplicate code from your tests. First, replace
the contents of test_getDogs_givenResponseStatusCode500_callsCompletion
with:

// when
let result = whenGetDogs(statusCode: 500)

// then
XCTAssertTrue(result.calledCompletion)
XCTAssertNil(result.dogs)
XCTAssertNil(result.error)

This method is simplified because the bulk of the work now happens within
whenGetDogs.

Next, replace the contents of


test_getDogs_givenError_callsCompletionWithError with this:

// given
let expectedError = NSError(domain: "com.DogPatchTests",
code: 42)

// when
let result = whenGetDogs(error: expectedError)

// then
XCTAssertTrue(result.calledCompletion)
XCTAssertNil(result.dogs)

let actualError = try XCTUnwrap(result.error as NSError?)


XCTAssertEqual(actualError, expectedError)

raywenderlich.com 161
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

This method is also simplified and now only handles the parts unique to setting up
the expectedError and testing that it returns correctly.

Build and run the tests, and they’ll all continue to pass. That was a great refactor,
and your upcoming tests will make good use of this helper method!

Deserializing models on success


You’re finally ready to handle the happy-path case: handling a successful response.

Before you do, there’s a convenience extension already in the project that you should
know about. Under DogPatchTests/Test Types/Extensions, open
Data+JSONFile.swift. You’ll see fromJSON(fileName:file:line:), a static method
for getting Data from a file.

This method is for tests. If the file can’t be found, then it will fail an assertion and
throw an exception. For example, this might happen if the file wasn’t added or the
wrong file name is input into the method.

Further, a kind developer colleague already provided a test data file called
GET_Dogs_Response.json for you within DogPatchTests/Data. You’re welcome. ;]

Armed with this infomation, you’re ready to write the happy-path test! Add the
following right after the previous test in DogPatchClientTests:

func test_getDogs_givenValidJSON_callsCompletionWithDogs()
throws {
// given
let data =
try Data.fromJSON(fileName: "GET_Dogs_Response")

let decoder = JSONDecoder()


let dogs = try decoder.decode([Dog].self, from: data)

// when
let result = whenGetDogs(data: data)

// then
XCTAssertTrue(result.calledCompletion)
XCTAssertEqual(result.dogs, dogs)
XCTAssertNil(result.error)
}

raywenderlich.com 162
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Here’s what this code does:

• First, you create data by calling Data.fromJSON with the given JSON filename.

• You create a new decoder of type JSONDecoder and use it to decode the data. This
is possible because Dog already conforms to Decodable and has tests verifying it
works in DogTests.swift.

• Then, you call whenGetDogs like the other tests, but this time, you pass data into
it.

• Finally, you assert that the completion is called, dogs is equal to the result.dogs
and the result.error is nil.

Build and run your tests and, as expected, you’ll see that this test fails. To make it
pass, replace the guard statement within getDogs(completion:) in
DogPatchClient with:

guard let response = response as? HTTPURLResponse,


response.statusCode == 200,
error == nil,
let data = data else {

The difference here is that you add let data as the condition for the guard to pass.

Then add the following after the guard block’s closing curly brace:

let decoder = JSONDecoder()


let dogs = try! decoder.decode([Dog].self, from: data)
completion(dogs, nil)

The try! statement here looks dangerous, and it definitely is! However, this is the
minimum amount of code to make the test pass, and it’s an indicator that you need
another test.

Build and run the unit tests, and they’ll all pass. There isn’t any refactoring to do, but
you need to get rid of that try!.

Under what condition would this try! be a problem? If the server returned a 200
response, but the JSON couldn’t be parsed into Dogs, this would cause the app to
crash.

raywenderlich.com 163
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Fortunately, this is exactly the type of problem that unit tests can catch and help you
prevent. Add the following test after the previous test to produce this exact scenario:

func test_getDogs_givenInvalidJSON_callsCompletionWithError()
throws {
// given
let data = try Data.fromJSON(
fileName: "GET_Dogs_MissingValuesResponse")

var expectedError: NSError!


let decoder = JSONDecoder()
do {
_ = try decoder.decode([Dog].self, from: data)
} catch {
expectedError = error as NSError
}

// when
let result = whenGetDogs(data: data)

// then
XCTAssertTrue(result.calledCompletion)
XCTAssertNil(result.dogs)

let actualError = try XCTUnwrap(result.error as NSError?)


XCTAssertEqual(actualError.domain, expectedError.domain)
XCTAssertEqual(actualError.code, expectedError.code)
}

Here’s a code breakdown:

• You set the data from GET_Dogs_MissingValuesResponse. This is a valid JSON


array, but it’s missing an id that’s required to deserialize a Dog object.

• Then, you create a decoder of type JSONDecoder and attempt to deserialize the
data. You capture the error that’s thrown as expectedError.

• You call whenGetDogs, and assert that the completion was called, the returned
dogs are nil and the error has the same domain and code as the expectedError.
You must cast to NSError because Error objects aren’t directly comparable. By
casting to NSError, you can compare the domain and code for the errors to one
another, which is “good enough” to show it’s the same error.

Build and run the tests. Not only does this test fail, it crashes! Well, it’s good you
caught this doing TDD rather than after the code shipped to production, right?

raywenderlich.com 164
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

To fix this issue, replace these lines within DogPatchClient:

let dogs = try! decoder.decode([Dog].self, from: data)


completion(dogs)

With this code instead:

do {
let dogs = try decoder.decode([Dog].self, from: data)
completion(dogs, nil)
} catch {
completion(nil, error)
}

Build and rerun your unit tests. See that they all now pass.

Dispatching to a response queue


Your DogPatchClient handles networking like a boss! There’s just one problem:
You’ve been mocking URLSessionTask to avoid making real networking calls, but
unfortunately, you’ve also masked a behavior of URLSessionTask.

URLSessionTask calls its closure on a background queue, which is problematic


because the app needs to perform UI operations using the Dogs or Error result,
which happens on the Main queue.

While you could leave it to the caller to dispatch to the main queue, this only pushes
the problem down the line and makes the networking client harder to use. A better
design is to have DogPatchClient accept a responseQueue and handle dispatching.
You can even do this without breaking your existing unit tests by making the
responseQueue optional.

Adding a response queue


Add the following test right after test_init_sets_session(), ignoring the
compiler error for now:

func test_init_sets_responseQueue() {
// given
let responseQueue = DispatchQueue.main

// when
sut = DogPatchClient(baseURL: baseURL,
session: mockSession,
responseQueue: responseQueue)
}

raywenderlich.com 165
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Since you haven’t defined responseQueue on DogPatchClient, this test currently


compiles. Ah, you did this dance earlier! ;]

To fix the error, add the following property to DogPatchClient after the others:

let responseQueue: DispatchQueue? = nil

Then replace the signature for init with this, ignoring the resulting compiler error
in the tests:

init(baseURL: URL,
session: URLSessionProtocol,
responseQueue: DispatchQueue?)

The tests don’t compile because you need to update setting sut in setUp. Replace
that line with:

sut = DogPatchClient(baseURL: baseURL,


session: mockSession,
responseQueue: nil)

Finally, add this code to the end of test_init_sets_responseQueue():

// then
XCTAssertEqual(sut.responseQueue, responseQueue)

Build and run the tests, and as expected, this test fails. To fix it, replace the let
responseQueue line within DogPatchClient with:

let responseQueue: DispatchQueue?

Then this line within init:

self.responseQueue = responseQueue

Build and rerun your tests, and they now pass.

raywenderlich.com 166
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Updating the mocks


Next, you need to update MockURLSession and MockURLSessionTask to call the
completion handler on a dispatch queue. In MockURLSession.swift, add this new
property to MockURLSession:

var queue: DispatchQueue? = nil

Next, add this method right below it:

func givenDispatchQueue() {
queue = DispatchQueue(label: "com.DogPatchTests.MockSession")
}

The existing tests don’t need this queue, so you only call this for the new tests you’ll
add next.

You also need to change the initializer for MockURLSessionTask. Replace it with the
following, ignoring the compiler error for now:

init(completionHandler:
@escaping (Data?, URLResponse?, Error?) -> Void,
url: URL,
queue: DispatchQueue?)

Then, replace this line within init:

self.completionHandler = completionHandler

With this code instead:

if let queue = queue {


self.completionHandler = { data, response, error in
queue.async() {
completionHandler(data, response, error)
}
}
} else {
self.completionHandler = completionHandler
}

If a queue passes into the initializer, you set self.completionHandler to dispatch


asynchronously to queue before calling completionHandler. This is similar to the
way a real URLDataTask dispatches to a dispatch queue.

raywenderlich.com 167
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

To fix the compiler error, replace the return statement within makeDataTask on
MockURLSession with:

return MockURLSessionTask(
completionHandler: completionHandler,
url: url,
queue: queue)

This code passes the queue into the new initializer on MockURLSessionTask.

Build and run your unit tests. Since none of the tests depend on which queue the
completion handler is called, they all continue to pass.

Handling dispatch scenarios


Next, you need to verify that completionHandler dispatches to the responseQueue,
which should happen in these cases:

1. An HTTP status code indicates a failure response.

2. An HTTP error is received.

3. A valid JSON response is received and successfully deserialized.

4. An invalid JSON response is received, and deserialization fails.

For the first case, in DogPatchClientTests.swift, add the following test after the
existing ones:

func
test_getDogs_givenHTTPStatusError_dispatchesToResponseQueue() {
// given
mockSession.givenDispatchQueue()
sut = DogPatchClient(baseURL: baseURL,
session: mockSession,
responseQueue: .main)

let expectation = self.expectation(


description: "Completion wasn't called")

// when
var thread: Thread!
let mockTask = sut.getDogs() { dogs, error in
thread = Thread.current
expectation.fulfill()
} as! MockURLSessionTask

let response = HTTPURLResponse(url: getDogsURL,


statusCode: 500,

raywenderlich.com 168
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

httpVersion: nil,
headerFields: nil)
mockTask.completionHandler(nil, response, nil)

// then
waitForExpectations(timeout: 0.1) { _ in
XCTAssertTrue(thread.isMainThread)
}
}

Here’s how this code works:

• In the given section, you call mockSession.givenDispatchQueue to set the


queue on mockSession. It, in turn, uses this to create a MockURLSessionTask. You
also create the sut, passing in .main as the responseQueue. Finally, you create an
expectation, which you’ll use later to wait for the completionHandler to be
called.

Technically, you could use any responseQueue. Pragmatically, you need to dispatch
the completion handler to the main queue. Sadly, iOS makes it difficult to check
which queue the code is currently running on… Oh, Apple! Don’t you know we need
this for unit tests?!

Fortunately, it’s easy to validate that the current Thread is the main thread, and the
main dispatch queue always runs on the main thread. Hence, your tests rely on this
fact to validate the code was “dispatched to the main queue.”

In reality, of course, you’re technically checking that the code runs on the main
Thread. However, short of Apple making it easier to test and validate which dispatch
queue is in use, this is “good enough.”

• In when, you first create a local variable for thread and then call sut.getDogs().
In its completion handler, you set thread and fulfill the expectation.

You then create a response variable with an error status code of 500 and use this to
call the completionHandler.

• In then, you call waitForExpectations to wait on the expectation to be


fulfilled. Inside the wait handler, you assert that the thread is the main thread.

Whoo, that was a bit of a whopper test! Build and run the unit tests, and you’ll see
that this test fails because you aren’t currently dispatching to the responseQueue on
DogPatchClient.

raywenderlich.com 169
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

To fix this, first replace this line within getDogs(completion:) on


DogPatchClient:

let task = session.makeDataTask(with: url) {


data, response, error in

With the following code, ignoring the warning:

let task = session.makeDataTask(with: url) { [weak self]


data, response, error in
guard let self = self else { return }

By using [weak self] and guard let self in this manner, you avoid creating the
strong reference cycle that’s possible if you instead reference self directly.

Next, inside the guard let response closure, replace the first instance of this code,
leaving the other two completion sites unchanged:

completion(nil, error)

With this code:

guard let responseQueue = self.responseQueue else {


completion(nil, error)
return
}
responseQueue.async {
completion(nil, error)
}

This code checks if the responseQueue is set and dispatches the call to the
completion if so.

Build and run the unit tests, and watch them all pass. There’s nothing to refactor yet,
so you can move on to testing the next scenario: ensuring an HTTP error dispatches
on the response queue.

Add the following test after the previous one:

func test_getDogs_givenError_dispatchesToResponseQueue() {
// given
mockSession.givenDispatchQueue()
sut = DogPatchClient(baseURL: baseURL,
session: mockSession,
responseQueue: .main)

let expectation = self.expectation(


description: "Completion wasn't called")

raywenderlich.com 170
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

// when
var thread: Thread!
let mockTask = sut.getDogs() { dogs, error in
thread = Thread.current
expectation.fulfill()
} as! MockURLSessionTask

let response = HTTPURLResponse(url: getDogsURL,


statusCode: 200,
httpVersion: nil,
headerFields: nil)
let error = NSError(domain: "com.DogPatchTests", code: 42)
mockTask.completionHandler(nil, response, error)

// then
waitForExpectations(timeout: 0.2) { _ in
XCTAssertTrue(thread.isMainThread)
}
}

This test is very similar to the previous one. However, in the when section, you pass
an error into the mockTask.completionHandler.

Build and run your tests, and, surprisingly, this test actually passes! What’s up with
that?

In getDogs, you’ll see that the check for an error and the HTTP status code are part
of the same guard statement, which looks like this:

guard let response = response as? HTTPURLResponse,


response.statusCode == 200,
error == nil,
let data = data else {

As a consequence, this coincidentally already dispatches the error to the


responseQueue.

Does this mean this test isn’t useful? No, it’s still useful. If you refactor this code
later, and this check isn’t combined in the same guard like it currently is, you still
want to ensure that the error dispatches on the responseQueue. So, leave this test
as is and move on to refactoring.

There is indeed code to refactor here!

raywenderlich.com 171
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

There’s a huge amount of duplicated code between these two tests. To fix this, add
the following helper method towards the top of the file, right after
whenGetDogs(...):

func verifyGetDogsDispatchedToMain(data: Data? = nil,


statusCode: Int = 200,
error: Error? = nil,
line: UInt = #line) {

mockSession.givenDispatchQueue()
sut = DogPatchClient(baseURL: baseURL,
session: mockSession,
responseQueue: .main)

let expectation = self.expectation(


description: "Completion wasn't called")

// when
var thread: Thread!
let mockTask = sut.getDogs() { dogs, error in
thread = Thread.current
expectation.fulfill()
} as! MockURLSessionTask

let response = HTTPURLResponse(url: getDogsURL,


statusCode: statusCode,
httpVersion: nil,
headerFields: nil)
mockTask.completionHandler(data, response, error)

// then
waitForExpectations(timeout: 0.2) { _ in
XCTAssertTrue(thread.isMainThread, line: line)
}
}

This method accepts inputs for data, statusCode and error. These will vary
depending on the actual behavior the test wants to verify. It also accepts an input for
line, which ensures XCTAssertTrue attributes a failure to the test method’s line
number instead of this helper method itself.

Now you can use this helper method to get rid of the duplicated code. Replace the
contents of test_getDogs_givenHTTPStatusError_dispatchesToResponseQueue
with:

verifyGetDogsDispatchedToMain(statusCode: 500)

raywenderlich.com 172
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Next, replace the contents of


test_getDogs_givenError_dispatchesToResponseQueue with:

// given
let error = NSError(domain: "com.DogPatchTests", code: 42)

// then
verifyGetDogsDispatchedToMain(error: error)

That’s much more readable and compact! Build and run the unit tests, and see they
continue to pass.

The next test scenario you need to cover is ensuring a valid response dispatches to
the response queue. Add the following test after the last one:

func test_getDogs_givenGoodResponse_dispatchesToResponseQueue()
throws {
// given
let data = try Data.fromJSON(
fileName: "GET_Dogs_Response")

// then
verifyGetDogsDispatchedToMain(data: data)
}

Nice! You make great use of your helper methods to write compact tests. Build and
run the tests, and you’ll see this test fails.

To fix this, replace this line within getDogs on DogPatchClient

completion(dogs, nil)

With the following:

guard let responseQueue = self.responseQueue else {


completion(dogs, nil)
return
}
responseQueue.async {
completion(dogs, nil)
}

Similar to how you handled the error, this code checks for a responseQueue and
dispatches dogs to it if so.

raywenderlich.com 173
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Build and run the tests. They now pass. However, now there’s duplicate logic in
DogPatchClient that you need to eliminate. To do so, add the following helper
method right after getDogs:

private func dispatchResult<Type>(


models: Type? = nil,
error: Error? = nil,
completion: @escaping (Type?, Error?) -> Void) {
guard let responseQueue = responseQueue else {
completion(models, error)
return
}
responseQueue.async {
completion(models, error)
}
}

You can use this method with any model because it uses a generic Type and accepts
inputs for models, error and completion. Regardless of inputs, it always checks if
there’s a responseQueue and dispatches the completion to it. If there’s no
responeQueue, it merely calls the completion with the inputs.

You can use this code to get rid of the duplicate code now. First, replace this code:

guard let responseQueue = self.responseQueue else {


completion(nil, error)
return
}
responseQueue.async {
completion(nil, error)
}

With the following:

self.dispatchResult(error: error, completion: completion)

Next, replace these lines:

guard let responseQueue = self.responseQueue else {


completion(dogs, nil)
return
}
responseQueue.async {
completion(dogs, nil)
}

raywenderlich.com 174
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

With the following:

self.dispatchResult(models: dogs, completion: completion)

Build and run your tests, and they still pass.

Finally, you need to verify that if an invalid JSON response is received, it’s also
dispatched to the response queue. Add the following test:

func
test_getDogs_givenInvalidResponse_dispatchesToResponseQueue()
throws {
// given
let data = try Data.fromJSON(
fileName: "GET_Dogs_MissingValuesResponse")

// then
verifyGetDogsDispatchedToMain(data: data)
}

Build and run your tests, and you’ll see this fails as anticipated. To fix this, replace
this line within getDogs in DogPatchClient:

completion(nil, error)

With the following:

self.dispatchResult(error: error, completion: completion)

Build and run your tests, and see them all pass. You did a great job refactoring
already, so there’s also nothing left to do refactor-wise here.

And guess what else? You just completed TDDing your networking client! Great job!

raywenderlich.com 175
iOS Test-Driven Development by Tutorials Chapter 8: RESTful Networking

Key points
In this chapter, you learned how to do TDD for a networking client. Here’s a recap of
what you learned:

• Avoid making real networking calls in your unit tests by mocking URLSession and
URLSessionTask.

• Do TDD for GET requests easily by breaking them into several smaller tasks:
Calling the right URL, handling HTTP status errors, handling valid and invalid
responses.

• Be careful about mocking URLSessionTask’s dispatch behavior to an internal


queue. You can work around this by creating your own dispatch queue on your
mocks.

• Dispatch to a response queue to make it easier for consumers to use your


networking client.

You’re one step closer to displaying those cute pups onscreen! In the next chapter,
you’ll learn how to do TDD for consuming the networking client in your view
controller.

raywenderlich.com 176
9 Chapter 9: Using the
Network Client
By Joshua Greene

In the last chapter, you identified that ListingsViewController isn’t actually doing
any networking. Rather, it has a // TODO comment in refreshData(). In response,
you created DogPatchClient to handle networking logic. However, you haven’t used
it yet.

In this chapter, you’ll update ListingsViewController to use DogPatchClient


upon refreshing! Specifically, you will:

• Add a shared instance on DogPatchClient.

• Add a network client property on ListingsViewController.

• Create a network client protocol.

• Create a mock network client using the protocol.

• Use the mock to stub and validate behavior.

Getting started
Feel free to use your project from the last chapter. If you want a fresh start, navigate
to this chapter’s starter directory, open the DogPatch subdirectory and then open
DogPatch.xcodeproj.

Once your project is ready, it’s time to jump in and set DogPatchClient up for
networking by adding a shared instance.

raywenderlich.com 177
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Creating a shared instance


While you could instantiate DogPatchClient directly, this has disadvantages:

1. You’d have to duplicate creation data, including the baseURL, session and
responseQueue, anywhere you instantiate DogPatchClient.

2. You’d make more network calls in parallel. As a result, you could use more
network data or harm battery life.

A better alternative is to add a static shared property on DogPatchClient. This uses


the “singleton plus” pattern: You’ll use the shared instance most of the time, but
you’ll also create one-off DogPatchClient instances, such as in your unit tests.

Before you can write app code, you first need to write a failing test. Open
DogPatchClientTests.swift and add this test right before
test_init_sets_baseURL(), ignoring the compiler error like usual:

func test_shared_setsBaseURL() {
// given
let baseURL = URL(
string: "https://dogpatchserver.herokuapp.com/api/v1/")!

// then
XCTAssertEqual(DogPatchClient.shared.baseURL, baseURL)
}

You first create an expected baseURL; this address corresponds to the real server URL
that you’ll be calling. You then assert DogPatchClient.shared.baseURL equals this
baseURL. Since you haven’t defined shared on DogPatchClient, however, this
doesn’t compile.

A compiler error counts as a failing test, so you’re allowed to write app code to fix it.

Open DogPatchClient.swift, and add the following right before


init(baseURL:session:responseQueue:):

static let shared = DogPatchClient(


baseURL: URL(string:"https://example.com")!,
session: URLSession(configuration: .default),
responseQueue: nil)

Here you’ve defined a static shared property with dummy values for its inputs. This
is enough to fix the compiler error in the unit tests.

raywenderlich.com 178
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Build and run the unit tests and, as expected, this last test fails. That’s because the
baseURL set on DogPatchClient.shared is not equal to the expected baseURL. To
make it pass, replace the baseURL value on shared with the following:

baseURL: URL(
string:"https://dogpatchserver.herokuapp.com/api/v1/")!

Warning: Due to the way URL(string:relativeTo:) resolves URLs, you


MUST include the trailing slash at the end of the URL string. If you don’t, the
URL created within getDogs won’t include the v1 in its path and, consequently,
the server will not recognize it.

Build and run the unit tests, and they should all pass now. However, you still need a
couple more tests to ensure you’ve set the correct values for
DogPatchClient.shared.

Add the following test below test_shared_setsBaseURL():

func test_shared_setsSession() {
XCTAssertTrue(
DogPatchClient.shared.session === URLSession.shared)
}

This test verifies DogPatchClient.shared.session has pointer equality to


URLSession.shared. Build and run the tests, and you’ll see this fails as expected. To
make it pass, open DogPatchClient.swift, and update the input argument for
session to URLSession.shared. Build and run the tests again, and verify they all
pass.

Finally, add the following test below test_shared_setsSession():

func test_shared_setsResponseQueue() {
XCTAssertEqual(DogPatchClient.shared.responseQueue, .main)
}

This test checks the final property on the shared instance, responseQueue. Build and
run this test, and you’ll see DogPatchClient.shared.responseQueue is currently
set to nil. To fix this, update the input parameter argument for responseQueue
to .main. Build and run the tests again to verify they all pass.

raywenderlich.com 179
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Ultimately, your static shared property on DogPatchClient should look like the
following:

static let shared = DogPatchClient(


baseURL: URL(
string:"https://dogpatchserver.herokuapp.com/api/v1/")!,
session: URLSession.shared,
responseQueue: .main)

Adding a network client property


Next, you need to add a networkClient property to ListingsViewController.
Before you can write app code, of course, you need a failing test.

Open ListingsViewControllerTests.swift and add the following right after //


MARK: - Instance Properties - Tests:

func test_networkClient_setToDogPatchClient() {
XCTAssertTrue(sut.networkClient === DogPatchClient.shared)
}

You assert that sut.networkClient has pointer equality to


DogPatchClient.shared. Since you haven’t defined networkClient on
ListViewController, this test won’t compile yet.

To fix this, open ListingsViewController.swift and add the following property right
after // MARK: - Instance Properties:

var networkClient =
DogPatchClient(baseURL: URL(string: "http://example.com")!,
session: URLSession.shared,
responseQueue: nil)

You declare this as a var to allow your tests to replace it with a mock object later on.
By defining this property, you’ve also fixed the compiler error.

Build and run the unit tests and you’ll see this test fails because networkClient
isn’t set to DogPatchClient .shared. To make it pass, replace the declaration for
var networkClient inside ListingsViewController.swift with the following:

var networkClient = DogPatchClient.shared

Build and run your tests again to verify they all pass.

raywenderlich.com 180
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Using the network client


While you could use DogPatchClient directly in your unit tests, this has several
drawbacks:

• You’d make real network calls that’d require an internet connection.

• The tests would fail if an internet connection wasn’t available or the server was
down.

• You wouldn’t be able to predict the network response in advance, so you couldn’t
verify the values are what you expected.

• Your unit tests would be slow to run because each would need to wait for a
network response.

Fortunately, there’s a better option: Use a mock network client. This lets you avoid
making real network calls while completely controlling the response results.

There are two ways you can create a mock network client in Swift:

1. You can create a mock by subclassing DogPatchClient and overriding each of its
methods. This works, but you may accidentally make real network calls if you
forget to override a method. You may also cause side effects, such as caching fake
network responses.

2. Similar to how you mocked URLSession, you can create a network client protocol
and use this instead of DogPatchClient directly. As a result, you’d eliminate the
possibility of making real network calls or causing side effects. Nice! The only
downside is that you need to create an extra protocol, but this is pretty quick and
easy to do.

In general, you should prefer to create a mock network client using a protocol over
subclassing-and-overriding.

One reason you might choose to subclass-and-override is if your app is tightly


coupled to the network client or its related types. For example, if you’re dealing with
a legacy app that has a lot of untested code. Even then, you should strive to replace
this using a protocol in the long run.

raywenderlich.com 181
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Creating the network client protocol


What should you put in the network client protocol? Any methods and properties
that callers need to use! In turn, you’ll be able to use your mock to validate that
you’re calling these correctly.

Okay, that’s enough theory! You’re ready to TDD the protocol now.

As always, you’ll write a test first. Open DogPatchClientTests.swift and add the
following method, right before test_shared_setsBaseURL(), ignoring the resulting
compiler error:

func test_conformsTo_DogPatchService() {
XCTAssertTrue((sut as AnyObject) is DogPatchService)
}

You cast sut as AnyObject to prevent a compiler warning, and you then assert sut
is DogPathcService. However, this causes a compiler error because you haven’t
defined DogPatchService yet.

To fix this, open DogPatchClient.swift and add the following, right before the class
declaration:

protocol DogPatchService {

Build and run the unit tests and, as expected, the last test will fail. To make it pass,
add the following to the end of DogPatchClient.swift, after the closing class curly
brace:

extension DogPatchClient: DogPatchService { }

Build and run the tests again, and verify they all pass.

This protocol isn’t very useful yet because it doesn’t have any methods. For this, add
the following test below test_conformsTo_DogPatchService():

func test_dogPatchService_declaresGetDogs() {
// given
let service = sut as DogPatchService

// then
_ = service.getDogs() { _, _ in }
}

raywenderlich.com 182
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

This test won’t compile because DogPatchService doesn’t know anything about
getDogs. To fix this, adding the following inside the DogPatchService protocol:

func getDogs(completion:
@escaping ([Dog]?, Error?) -> Void) -> URLSessionTaskProtocol

Build and run your tests now, and they should all pass.

But wait! Won’t this test result in real networking calls being made? No, if you check
the setUp(), you’ll recall that you passed an instance of MockSession to create
DogPatchClient and set this to sut. Since MockSession doesn’t make any real
networking calls, you can freely call methods on DogPatchClient without worry.

Are there any other properties or methods you should add to DogPatchService? For
example, what about init(baseURL:session:responseQueue:) or the shared
property?

No, you don’t need to add these because they are implementation details. A
consumer doesn’t need to know how you constructed its dependency. Rather, it only
need to know what behavior the dependency provides. This, in turn, defines which
methods and properties go into the protocol.

For now, this one method is all you need in DogPatchService!

Creating the mock network client


You next need to create the mock network client. Your first step is to write a test
for… Oh, wait! You don’t need a test. ;]

Just like MockURLSession, your mock network client won’t be part of your app code.
Instead, it enables you to write unit tests, and this in turn enables you to write app
code. Okay, carry on then…!

Under DogPatchTests/Test Types/Mocks, create a new Swift File called


MockDogPatchService, and replace its contents with the following:

@testable import DogPatch


import Foundation

// 1
class MockDogPatchService: DogPatchService {

// 2
var baseURL = URL(string: "https://example.com/api/")!
var getDogsCallCount = 0

raywenderlich.com 183
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

var getDogsCompletion: (([Dog]?, Error?) -> Void)!


lazy var getDogsDataTask = MockURLSessionTask(
completionHandler: { _, _, _ in },
url: URL(string: "dogs", relativeTo: baseURL)!,
queue: nil)

// 3
func getDogs(completion: @escaping ([Dog]?, Error?) -> Void)
-> URLSessionTaskProtocol {
getDogsCallCount += 1
getDogsCompletion = completion
return getDogsDataTask
}
}

Here’s what you’ve done:

1. You create a new type for MockDogPatchService that conforms to


DogPatchService.

2. You add properties for baseURL, getDogsCallCount, getDogsCompletion and


getDogsDataTask. You’ll use them to verify the mock gets called as expected and
return stubbed responses.

3. You implement getDogs(completion:), which DogPatchService requires.


Whenever it’s called, you increment getDogsCallCount, set getDogsCompletion
and return getDogsDataTask.

Fantastic, you implemented this mock like a pro! It mirrors how DogPatchClient
works, but it allows you to fully control the response that’s returned, doesn’t require
a network connection and doesn’t have any network delay. So now, it’s time to put it
to work.

Using the mock network client


You’re finally ready to use the mock network client!

Open ListingsViewControllerTests.swift, and you’ll see that several tests are


already included for the existing functionality. Your job is to do TDD for
refreshData().

raywenderlich.com 184
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Your first test will assert that the view controller holds onto the returned data task.
To do this, add the following code right after
test_viewWillAppear_calls_refreshData():

func test_refreshData_setsRequest() {
// given
let mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient
}

Here, you create mockNetworkClient and attempt to set this as


sut.networkClient. Unfortunately, this causes a compiler error. What’s up with
that?

Xcode actually gives a helpful error message:

Cannot assign value of type 'MockDogPatchService' to type


'DogPatchClient'

The compiler expects networkClient to be of type DogPatchClient, yet you’re


attempting to set it to MockDogPatchClient, which doesn’t inherit from
DogPatchClient. To fix this error, you need to explicitly set the type of
networkClient to be DogPatchService.

Open ListingsViewController.swift and replace this line:

var networkClient = DogPatchClient.shared

With the following:

var networkClient: DogPatchService = DogPatchClient.shared

Both MockDogPatchService and DogPatchClient conform to DogPatchService, so


this eliminates the compiler error. However, you’ll notice that
test_networkClient_setToDogPatchClient no longer compiles because Swift
cannot use the identical-to operator, ===, to compare DogPatchClient and
DogPatchService. To fix this, you need to cast the protocol type to the object type
you want to compare. Replace the contents of
test_networkClient_setToDogPatchClient with:

XCTAssertTrue((sut.networkClient as? DogPatchClient)


=== DogPatchClient.shared)

Run your tests and they should all pass again.

raywenderlich.com 185
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Next, add this next code within test_refreshData_setsRequest(), right before its
closing method brace:

// when
sut.refreshData()

// then
XCTAssertTrue(sut.dataTask ===
mockNetworkClient.getDogsDataTask)

This checks that sut.dataTask is set to mockNetworkClient.getDogsDataTask


after you’ve called sut.refreshData(). Since you haven’t declared dataTask on
ListingsViewController, however, this doesn’t compile. To fix this, open
ListingsViewController.swift and add the following right after var viewModels:

var dataTask: URLSessionTaskProtocol?

This fixes the compiler error, so build and run the tests and verify that it fails. To
make it pass, you need to set dataTask whenever refreshData() is called.

Replace the contents of refreshData() with the following:

dataTask = networkClient.getDogs() { dogs, error in

Build and run the tests again, and they’ll all pass.

It’s possible refreshData could be called more than once in quick succession. For
example, this could happen if the user “pulls to refresh” when a network call is
already in progress.

If dataTask is already set, you don’t want to call getDogs multiple times. Add the
following test after test_refreshData_setsRequest(); it ensure you’re only
calling getDogs once, even if refreshData is called in quick succession:

func test_refreshData_ifAlreadyRefreshing_doesntCallAgain() {
// given
let mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient

// when
sut.refreshData()
sut.refreshData()

// then
XCTAssertEqual(mockNetworkClient.getDogsCallCount, 1)
}

raywenderlich.com 186
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

This test calls refreshData twice in succession to simulate that scenario.

Build and run the unit tests to verify this one fails. To make it pass, open
ListingsViewController.swift and add the following code just after the opening
curly brace for refreshData():

guard dataTask == nil else { return }

This guard returns early if dataTask is not nil. Build and run your unit tests, and
they should all now pass.

Do you see anything that needs to be refactored? The app code looks fine, but what
about the unit tests? Yep, you’ve duplicated the code for setting sut.networkClient
to mockNetworkClient.

To eliminate this duplication, first add this new property right after the var sut
line:

var mockNetworkClient: MockDogPatchService!

Next, add the following method right after the givenDogs(count:) method:

func givenMockNetworkClient() {
mockNetworkClient = MockDogPatchService()
sut.networkClient = mockNetworkClient
}

You won’t need a MockDogPatchService for every test, so you add this helper
method to create and set one. You’ll call this only from the tests that require a mock.

Then, add the following within tearDown(), right after its opening method brace:

mockNetworkClient = nil

This ensures mockNetworkClient is set to nil after each test run completes.

Finally, replace the following two lines in both test_refreshData_setsRequest


and test_refreshData_ifAlreadyRefreshing_doesntCallAgain:

let mockNetworkClient = MockDogPatchService()


sut.networkClient = mockNetworkClient

With this one line instead:

givenMockNetworkClient()

raywenderlich.com 187
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

This gets rid of the duplicate code. Now, build and run the tests to verify they still
pass.

For the next test, you need to ensure that dataTask is set back to nil after the
completion is called for getDogs. Add the following test below
test_refreshData_ifAlreadyRefreshing_doesntCallAgain():

func test_refreshData_completionNilsDataTask() {
// given
givenMockNetworkClient()
let dogs = givenDogs()

// when
sut.refreshData()
mockNetworkClient.getDogsCompletion(dogs, nil)

// then
XCTAssertNil(sut.dataTask)
}

Here’s how this test works:

1. Within the given section, you make excellent use of your helper methods to
create mockNetworkClient and dogs.

2. Within when, you first call sut.refreshData() to set the dataTask. You then
pass dogs to the getDogsCompletion closure on the mockNetworkClient. This
executes the passed-in closure from ListingsViewController, and it should set
the dataTask to nil.

3. Within then, you assert that the sut.dataTask is set back to nil.

Build and run this test, and you’ll see it fails. Of course, that’s because you haven’t
actually set dataTask to nil within the getDogs completion closure.

To make this pass, add this line right inside the completion closure within
refreshData on ListingsViewController:

self.dataTask = nil

Build and run the tests to verify the last one now passes.

raywenderlich.com 188
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

You’re now ready to test the “happy path”, which returns dogs successfully and sets
it on the ListingsViewController. Add the following test below
test_refreshData_completionNilsDataTask():

func test_refreshData_givenDogsResponse_setsViewModels() {
// given
givenMockNetworkClient()
let dogs = givenDogs()
let viewModels = dogs.map { DogViewModel(dog: $0) }

// when
sut.refreshData()
mockNetworkClient.getDogsCompletion(dogs, nil)

// then
XCTAssertEqual(sut.viewModels, viewModels)
}

Here’s how this test works:

1. Within given, you use your helper methods to create mockNetworkClient and
dogs, then you create viewModels by mapping each dog to a DogViewModel.

2. For the when section, you call sut.refreshData() and execute the
getDogsCompletion with the given dogs.

3. Finally in then, you assert that sut.viewModels is equal to viewModels.

Build and run this test to verify it fails. You need to set viewModels on
ListingsViewController to make it pass.

Add the following right after dataTask = nil within the refreshData() on
ListingsViewController:

self.viewModels = dogs?.map { DogViewModel(dog: $0) } ?? []

This likewise calls map to turn dogs into a DogViewModel array. If there’s an error,
dogs might be nil so you use the optional unwrap operator ? and provide the
default value as an empty array.

Build and run your tests, and you’ll see this last one now passes.

Is there anything more to refactor here? Well, maybe…

At first glance, the code between test_refreshData_completionNilsDataTask and


test_refreshData_completionNilsDataTask looks similar. You’ve already
factored out several helper methods, however, and you’re using them here.

raywenderlich.com 189
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

While you could try to refactor these tests further, you’d likely make them harder to
read. Consequently, it’s okay to leave them as is! There’s always a balancing act
between refactoring as much as possible and inline readability. If you’re ever in
doubt, try refactoring! If it turns out the code is too difficult to read, you can always
undo the change.

For the next test, you’ll verify you reload the tableView after the viewModels are
set. Add the following test below
test_refreshData_givenDogsResponse_setsViewModels():

func test_refreshData_givenDogsResponse_reloadsTableView() {
// given
givenMockNetworkClient()
let dogs = givenDogs()

// 1
class MockTableView: UITableView {
var calledReloadData = false
override func reloadData() {
calledReloadData = true
}
}
// 2
let mockTableView = MockTableView()
sut.tableView = mockTableView

// when
sut.refreshData()
mockNetworkClient.getDogsCompletion(dogs, nil)

// then

// 3
XCTAssertTrue(mockTableView.calledReloadData)
}

There are three significant parts to this test:

1. First, you create a MockTableView to override reloadData(). Inside that, you


update a Boolean for calledReloadData.

2. Next, you create a new instance for mockTableView and set this as
sut.tableView to ensure it’s used.

3. Finally, after you’ve called refreshData() and executed the


getDogsCompletion, you assert that mockTableView.calledReloadData is true.

raywenderlich.com 190
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Build and run the unit tests, and you’ll see this test fails because you don’t currently
call reloadData on the tableView. To get this to pass, add the following line, right
after setting viewModels within refreshData() on ListingsViewController:

self.tableView.reloadData()

Build and run the tests again, and this last one will now pass.

Okay, you’re finally ready to check out the app. Build and run it! You’ll see that the
dogs… don’t show?!

Instead, the view controller shows an error screen, and if you “pull down to refresh,”
you’ll see the “loading indicator” never disappears.

This is because of the way that you implemented


tableView(_:numberOfRowsInSection:): It considers whether or not the
tableView is refreshing.

raywenderlich.com 191
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

To fix this, you need to begin and end refreshing on the table view’s
refreshControl. Add the following test below
test_refreshData_givenDogsResponse_reloadsTableView():

func test_refreshData_beginsRefreshing() {
// given
givenMockNetworkClient()

// when
sut.refreshData()

// then
XCTAssertTrue(sut.tableView.refreshControl!.isRefreshing)
}

This test verifies that isRefreshing on the refreshControl is true after calling
refreshData(). Build and run this test, and it will fail because you haven’t started
refreshing yet.

To fix this, add the following line right after the guard statement within
refreshData() on ListingsViewController:

tableView.refreshControl?.beginRefreshing()

Build and run the test again, and they’ll all pass.

Lastly, you need to end refreshing whenever your code calls the completion closure.
Add the following test to verify this below
test_refreshData_beginsRefreshing():

func test_refreshData_givenDogsResponse_endsRefreshing() {
// given
givenMockNetworkClient()
let dogs = givenDogs()

// when
sut.refreshData()
mockNetworkClient.getDogsCompletion(dogs, nil)

// then
XCTAssertFalse(sut.tableView.refreshControl!.isRefreshing)
}

This test calls refreshData(), executes the getDogsCompletion closure and asserts
that isRefreshing on the refreshControl is false. Build and run this test, and it
will fail because you haven’t actually finished refreshing yet.

raywenderlich.com 192
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

To make it pass, add the following line right after setting viewModels within
refreshData() on ListingsViewController:

self.tableView.refreshControl?.endRefreshing()

Build and run your tests again, and they should all pass.

Great job! You’ve done TDD for the entire refreshData() implementation. Build
and run the app to see it in action.

raywenderlich.com 193
iOS Test-Driven Development by Tutorials Chapter 9: Using the Network Client

Key points
In this chapter, you learned how to TDD using a network client. Here are the key
points you covered:

• You created a shared instance for the network client to avoid having multiple
instances throughout the app.

• You avoided using the real network client directly in your unit tests since that
would require an internet connection, which would cause them to be slower and
make testing responses harder. You used a mock network client instead.

• You learned why it’s better to create a mock network client by implementing a
protocol, instead of subclassing and overriding. By doing so, you avoided
accidentally making real network calls and side effects such as caching.

You’re now able to display network results on screen! Wouldn’t it be nice if you could
also see the images of the pups instead of just a placeholder image? You bet it would!
In the next chapter, you’ll learn how to create an image client to help you do just
that.

raywenderlich.com 194
10 Chapter 10: ImageClient
By Joshua Greene

In the last chapter, you used DogPatchClient to download and display dogs. Each
Dog has an imageURL, but you haven’t used it so far. While you could download
images by making network requests directly within ListingsViewController, you
wouldn’t be able to use that logic anywhere else.

Instead, you’ll do TDD to create an ImageClient for handling images. You can use
ImageClient anywhere you need it in the app.

You’ll work through these steps in this chapter:

• Set up the image client.

• Create an image client protocol.

• Download an image from a URL.

• Cache tasks and images based on their URL.

• Set an image from a URL on an image view.

• Use the image client to display images.

Getting started
Feel free to use your project from the last chapter. If you want a fresh start, navigate
to this chapter’s starter directory, open the DogPatch subdirectory and then open
DogPatch.xcodeproj.

raywenderlich.com 195
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Your first step is going to be to get everything set up for your image client. Here’s
how.

Setting up the image client


Another developer (ahem, you’re welcome) has already done TDD for ImageClient
and its properties. To keep the focus on new concepts, this section will fast-track you
through adding this code.

Under DogPatch/Networking, create a new Swift File called ImageClient.swift


and replace its contents with the following:

// 1
import UIKit

class ImageClient {

// MARK: - Static Properties


// 2
static let shared = ImageClient(responseQueue: .main,
session: URLSession.shared)

// MARK: - Instance Properties


// 3
var cachedImageForURL: [URL: UIImage]
var cachedTaskForImageView:
[UIImageView: URLSessionTaskProtocol]

let responseQueue: DispatchQueue?


let session: URLSessionProtocol

// MARK: - Object Lifecycle


// 4
init(responseQueue: DispatchQueue?,
session: URLSessionProtocol) {

self.cachedImageForURL = [:]
self.cachedTaskForImageView = [:]

self.responseQueue = responseQueue
self.session = session
}
}

raywenderlich.com 196
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Here’s what this does:

1. You first import UIKit to access UIImage and UIImageView, next you create a
new class for ImageClient.

2. You next declare a static property for shared. You’ll use this in your app code,
but you’ll create one-off instances in your unit tests. This is just like
DogPatchClient.

3. You then declare two cache properties, cachedImageForURL and


cachedTaskForImageView. You also declare one property for session, which
you’ll use to make the networking calls, and one for responseQueue, which you’ll
use to dispatch the results.

4. Last, you create an initializer that sets each property.

You also need to add the tests for this class. Under DogPatchTests/Cases/
Networking, create a new Swift File called ImageClientTests.swift and replace its
contents with the following:

// 1
@testable import DogPatch
import XCTest

class ImageClientTests: XCTestCase {

// 2
var mockSession: MockURLSession!
var sut: ImageClient!

// MARK: - Test Lifecycle


// 3
override func setUp() {
super.setUp()
mockSession = MockURLSession()
sut = ImageClient(responseQueue: nil,
session: mockSession)
}

override func tearDown() {


mockSession = nil
sut = nil
super.tearDown()
}

// MARK: - Static Properties - Tests


// 4
func test_shared_setsResponseQueue() {
XCTAssertEqual(ImageClient.shared.responseQueue, .main)
}

raywenderlich.com 197
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

func test_shared_setsSession() {
XCTAssertTrue(ImageClient.shared.session ===
URLSession.shared)
}

// MARK: - Object Lifecycle - Tests


// 5
func test_init_setsCachedImageForURL() {
XCTAssertTrue(sut.cachedImageForURL.isEmpty)
}

func test_init_setsCachedTaskForImageView() {
XCTAssertTrue(sut.cachedTaskForImageView.isEmpty)
}

func test_init_setsResponseQueue() {
XCTAssertTrue(sut.responseQueue === nil)
}

func test_init_setsSession() {
XCTAssertTrue(sut.session === mockSession)
}
}

Here’s how this works:

1. You import both DogPatch and XCTest and create a test class for
ImageClientTests.

2. You declare two instance properties: mockSession keeps hold of a


MockURLSession, which you’ll use instead of making real networking calls, and
sut keeps hold of the ImageClient you’re testing.

3. You set each instance property within setUp() and nil them within tearDown().

4. You create tests that validate that the shared instance has expected values.

5. Lastly, you create tests to validate that the initializer sets properties as expected.

Build and run your tests to verify that they all pass.

Wow, you covered a lot in short amount of time! While this code is definitely
important, you learned how to do TDD for this in previous chapters. You’re now
ready to dive into new concepts for this chapter!

raywenderlich.com 198
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Creating an image client protocol


Similar to DogPatchClient, you’ll create a protocol for the ImageClient to enable
you to mock and verify its use.

As always, you first need to write a failing test. Add the following to
ImageClientTests, right after the last test method:

// MARK: - ImageService - Tests


func test_conformsTo_ImageService() {
XCTAssertTrue((sut as AnyObject) is ImageService)
}

You cast sut as AnyObject to prevent a compiler warning and then assert this
conforms to ImageService. However, this doesn’t compile because you haven’t
declared ImageService.

To fix this, add the following to the top of ImageClient.swift after the imports:

protocol ImageService {

Build and run the tests to validate the last one fails.

To make it pass, add the following after the class closing curly brace for
ImageClient:

// MARK: - ImageService
extension ImageClient: ImageService {

Build and run the tests again to verify the last one now passes. There’s nothing to
refactor, so you can simply continue.

You next need a test to define the downloadImage method signature. Open
ImageClientTests.swift and add the following right after the last test:

func test_imageService_declaresDownloadImage() {
// given
let url = URL(string: "https://example.com/image")!
let service = sut as ImageService

// then
_ = service.downloadImage(fromURL: url) { _, _ in }
}

raywenderlich.com 199
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

You create service by casting sut as ImageService and then call


service.downloadImage to verify the method exists.

Since you’ve yet to declare this method, this causes a compiler error. Open
ImageClient.swift and add the following code within ImageService to fix this:

func downloadImage(
fromURL url: URL,
completion: @escaping (UIImage?, Error?) -> Void)
-> URLSessionTaskProtocol

You also need to make ImageClient implement this method to make it conform to
ImageService. Replace the extension on ImageClient with the following:

extension ImageClient: ImageService {


func downloadImage(
fromURL url: URL,
completion: @escaping (UIImage?, Error?) -> Void)
-> URLSessionTaskProtocol {
let url = URL(string: "https://example.com")!
return session.makeDataTask(with: url, completionHandler:
{ _, _, _ in })
}
}

You call session.makeDataTask with dummy value to simply make it compile.

Build and run the tests again to verify they all pass. Lastly, you need one more
method, to set an image onto an image view from a URL. Open
ImageClientTests.swift and add this test at the end:

func test_imageService_declaresSetImageOnImageView() {
// given
let service = sut as ImageService
let imageView = UIImageView()
let url = URL(string: "https://example.com/image")!
let placeholder = UIImage(named: "image_placeholder")!

// then
service.setImage(on: imageView,
fromURL: url,
withPlaceholder: placeholder)
}

raywenderlich.com 200
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

This test will verify that a new method, setImage(on:fromURL:withPlaceholder),


exists. This doesn’t compile because you haven’t declared it in the protocol. To fix it,
add the following to ImageService after downloadImage:

func setImage(on imageView: UIImageView,


fromURL url: URL,
withPlaceholder placeholder: UIImage?)

You also need to add this method to ImageClient to make it compile. Add this to
ImageClient after downloadImage:

func setImage(on imageView: UIImageView,


fromURL url: URL,
withPlaceholder placeholder: UIImage?) {

You create setImage as an empty method because that’s the easiest way to
implement it.

Build and run the tests to confirm they all compile and pass.

Is there anything to refactor? Yes, you duplicated service and url within the last
two tests. To fix this, add the following after the sut property:

var service: ImageService {


return sut as ImageService
}
var url: URL!

You also need to set url before each test run. Add this line to setUp, right before
setting sut:

url = URL(string: "https://example.com/image")!

After each test run, you need to reset url. Add this line to tearDown, again before
setting sut:

url = nil

You can now use these properties within your tests. Delete the entire given section
from test_imageService_declaresDownloadImage and delete the lines for
service and url from test_imageService_declaresSetImageOnImageView.

Finally, build and run the tests to ensure they still pass.

raywenderlich.com 201
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Downloading an image
You next need to implement downloadImage(fromURL:completion:).

For the first test, you’ll validate that session creates the a
URLSessionTaskProtocol using the passed-in url. Add this code after the last test
in ImageClientTests:

func test_downloadImage_createsExpectedTask() {
// when
let dataTask = sut.downloadImage(fromURL: url) { _, _ in }
as? MockURLSessionTask

// then
XCTAssertEqual(dataTask?.url, url)
}

You cast sut.downloadImage to a MockURLSessionTask, set this to dataTask and


then assert dataTask?.url equals url.

Remember how you used mockSession to create ImageClient in this test’s setup?
MockURLSession always returns a MockURLSessionTask whenever its
dataTask(with:completionHandler:) is called.

Build and run this test and you’ll see it fails. This is because you aren’t using the
passed-in url when you call makeDataTask on the ImageClient.

To fix this, open ImageClient.swift and replace the contents of downloadImage with
the following:

let task = session.makeDataTask(with: url) {


data, response, error in

}
return task

Here you call makeDataTask and provide named arguments for the closure
parameters; you’ll use these soon. Similarly, you name the returned value from
makeDataTask simply as task.

Build and run the tests now, and the last one should pass.

raywenderlich.com 202
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

You also need to call resume on the task to start it. Open ImageClientTests.swift
and add the following test to the end of class:

func test_downloadImage_callsResumeOnTask() {
// when
let dataTask =
sut.downloadImage(fromURL: url) { _, _ in }
as? MockURLSessionTask

// then
XCTAssertTrue(dataTask?.calledResume ?? false)
}

This time, you call downloadImage and verify calledResume is set to true;
calledResume is a property you added to MockURLSessionTask in a previous
chapter.

Build and run to ensure this test fails. To make it pass, you actually need to call
resume() on the task. Add the following right before the return statement within
downloadImage on ImageClient:

task.resume()

Build and run the tests again to verify the last one passes.

Do you see anything to refactor? Yep, you’ve duplicated the when code in the last two
tests. You’re going to call downloadImage a lot, so it’s best to pull this into a helper
method.

Before you do, you first need to add a few properties. Add the following right after
the other properties on ImageClientTests:

var receivedTask: MockURLSessionTask?


var receivedError: Error?
var receivedImage: UIImage?

You also need to ensure you release these after each test. Add these lines within
tearDown() right after setting sut:

receivedTask = nil
receivedError = nil
receivedImage = nil

raywenderlich.com 203
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

You can now write the helper method. Add the following right after tearDown():

// MARK: - When
// 1
func whenDownloadImage(
image: UIImage? = nil, error: Error? = nil) {

// 2
receivedTask = sut.downloadImage(
fromURL: url) { image, error in

// 3
self.receivedImage = image
self.receivedError = error
} as? MockURLSessionTask

// 4
guard let receivedTask = receivedTask else {
return
}
if let image = image {
receivedTask.completionHandler(
image.pngData(), nil, nil)

} else if let error = error {


receivedTask.completionHandler(nil, nil, error)
}
}

Here’s how this works:

1. You declare a new method for whenDownloadImage. It takes two inputs, image
and error.

2. You call sut.downloadImage, cast its return value to MockURLSessionTask and


set this to receivedTask.

3. You set receivedImage and receivedError in the completion for


downloadImage.

4. Lastly, you guard that receivedTask is set. If so, you then check if image is set
and call completionHandler with it. Otherwise, you check if error is set and call
completionHandler with it instead.

raywenderlich.com 204
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

You’re now ready to use this helper method to refactor your tests! Replace the
contents of test_downloadImage_createsExpectedTask with the following:

// when
whenDownloadImage()

// then
XCTAssertEqual(receivedTask?.url, url)

This is much nicer to read! You simply call whenDownloadImage and then assert
receivedTask?.url equals the expected url.

Next, replace the contents of test_downloadImage_callsResumeOnTask with this:

// when
whenDownloadImage()

// then
XCTAssertTrue(receivedTask?.calledResume ?? false)

Nice! You again reuse whenDownloadImage() and then assert


receivedTask?.calledResume is true. In the case receivedTask is nil, then you
default to false to cause the test to fail.

Handling the happy path


You’re now ready to handle the happy path, downloading an image successfully. Add
this test next:

func test_downloadImage_givenImage_callsCompletionWithImage() {
// given
let expectedImage = UIImage(named: "happy_dog")!

// when
whenDownloadImage(image: expectedImage)

// then
XCTAssertEqual(expectedImage.pngData(),
receivedImage?.pngData())
}

Here, you create an expectedImage, call whenDownloadImage with it and then assert
that expectedImage and receivedImage have the same pngData(). Since UIImage
uses object equality, you cannot compare images directly. However, you can compare
their underlying data to verify they’re the same.

raywenderlich.com 205
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Build and run the test to verify it fails. To make it pass, you need to actually create an
image from the passed-in data and call the completion with it.

Add the following inside the session.dataTask closure within downloadImage on


ImageClient:

if let data = data,


let image = UIImage(data: data) {
completion(image, nil)
}

Here, you verify that data is set and try to create an image from it. If this succeeds,
you call completion with it.

Build and run the tests to verify the test now passes.

Handling the error path


You also need to handle the error case. Add this test right after the last one:

func test_downloadImage_givenError_callsCompletionWithError() {
// given
let expectedError = NSError(domain: "com.example",
code: 42,
userInfo: nil)

// when
whenDownloadImage(error: expectedError)

// then
XCTAssertEqual(expectedError, receivedError as NSError?)
}

This is similar to the previous test, except this time you’re passing an
expectedError into whenDownloadImage and asserting receivedError equals
expectedError.

Build and run this test to confirm it fails. To get it to pass, add the following code
inside the completion closure within downloadImage on ImageClient, right after
the closing curly brace for if let data:

else {
completion(nil, error)
}

Build and run the tests again, and they should all pass now.

raywenderlich.com 206
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Dispatching an image
Next, you need to ensure that completion dispatches to the responseQueue
whenever your app successfully downloads an image. Add this test to verify this:

func test_downloadImage_givenImage_dispatchesToResponseQueue() {
// given
mockSession.givenDispatchQueue()
sut = ImageClient(responseQueue: .main,
session: mockSession)
let expectedImage = UIImage(named: "happy_dog")!
var receivedThread: Thread!
let expectation = self.expectation(
description: "Completion wasn't called")

// when
let dataTask = sut.downloadImage(fromURL: url) { _, _ in
receivedThread = Thread.current
expectation.fulfill()

} as! MockURLSessionTask
dataTask.completionHandler(expectedImage.pngData(), nil, nil)

// then
waitForExpectations(timeout: 0.2)
XCTAssertTrue(receivedThread.isMainThread)
}

Here’s how this test works:

• Within given, you first call mockSession.givenDispatchQueue(). This tells


mockSession to create a MockURLSessionTask that dispatches its
completionHandler on an internal queue. Then, you create sut, passing .main for
its responseQueue and mockSession for its session. Lastly, you create
expectedImage, receivedThread and expectation.

• Within when, you call sut.downloadImage. Inside its completion, you set
receivedThread and fulfill the expectation. You then execute
dataTask.completionHandler with image.pngData().

• Within then, you wait until the expectation is fulfilled. Afterwards, you assert
receivedThread.isMainThread.

raywenderlich.com 207
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

This is similar to how you verified DogPatchClient dispatched to its


responseQueue. While you can’t directly get the dispatch queue your code is
executing, you can get the current thread and check if it’s the main thread. Until
Apple provides a way to check the current dispatch queue, this is “good enough” for
test purposes.

Build and run this test to verify it fails. To make it pass, replace this code within
downloadImage on ImageClient:

let task = session.makeDataTask(with: url) {


data, response, error in
if let data = data,
let image = UIImage(data: data) {
completion(image, nil)
}

with the following code instead, being careful not to change any code before or after
this:

let task =
session.makeDataTask(with: url) {
// 1
[weak self] data, response, error in
guard let self = self else { return }

if let data = data, let image = UIImage(data: data) {


// 2
if let responseQueue = self.responseQueue {
responseQueue.async { completion(image, nil) }

// 3
} else {
completion(image, nil)
}
}

You made two changes here:

1. You first declare [weak self] and then immediately call guard let self within
the closure. This prevents a strong reference cycle due to capturing self.

2. If you’re able to create an image, you check if responseQueue is set and dispatch
completion to it.

3. If responseQueue isn’t set, you call the completion directly.

raywenderlich.com 208
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Build and run the tests, and the last one will now pass.

For the refactor step, you’ll move expectedImage into a property to get rid of the
duplicated code. Add this line after the other properties:

var expectedImage: UIImage!

You need to release this in tearDown(), so add this right before calling
super.tearDown():

expectedImage = nil

Lastly, add this code right after tearDown():

// MARK: - Given
func givenExpectedImage() {
expectedImage = UIImage(named: "happy_dog")!
}

Great! You can now use this helper method to get rid of the duplication in the tests.
Replace the lines for let expectedImage = everywhere in ImageClientTests with
the following:

givenExpectedImage()

Build and run the tests, and they should all continue to pass.

Dispatching an error
You also need to verify errors are dispatched to the responseQueue. Add this test
right after the last one:

func test_downloadImage_givenError_dispatchesToResponseQueue() {
// given
mockSession.givenDispatchQueue()
sut = ImageClient(responseQueue: .main,
session: mockSession)

let error = NSError(domain: "com.example",


code: 42,
userInfo: nil)
var receivedThread: Thread!
let expectation = self.expectation(
description: "Completion wasn't called")

// when
let dataTask = sut.downloadImage(fromURL: url) { _, _ in

raywenderlich.com 209
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

receivedThread = Thread.current
expectation.fulfill()
} as! MockURLSessionTask
dataTask.completionHandler(nil, nil, error)

// then
waitForExpectations(timeout: 0.2)
XCTAssertTrue(receivedThread.isMainThread)
}

This test is very similar to the success case. The main difference is that you’re
passing an error to the dataTask.completionHandler instead of an image.

Build and run the tests to verify this fails. Then, replace this code within
downloadImage onImageClient:

completion(nil, error)

With this instead:

if let responseQueue = self.responseQueue {


responseQueue.async { completion(nil, error) }

} else {
completion(nil, error)
}

Build and run the tests again to verify they all pass.

You’ve duplicated logic in both the app and test code, so you need to refactor it!

You’ll first update the app code. Specifically, you need a new method to handle
dispatching to the responseQueue. Add the following right after downloadImage:

private func dispatch(


image: UIImage? = nil,
error: Error? = nil,
completion: @escaping (UIImage?, Error?) -> Void) {

guard let responseQueue = responseQueue else {


completion(image, error)
return
}
responseQueue.async { completion(image, error) }
}

This method accepts an image, error and completion. It then verifies if


responseQueue is set. If it’s not, it calls completion directly. If it is, then it
dispatches completion to the responseQueue.

raywenderlich.com 210
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

You can now use this to remove the duplicate app logic. Replace these lines in
downloadImage on ImageClient:

if let responseQueue = self.responseQueue {


responseQueue.async { completion(image, nil) }

} else {
completion(image, nil)
}

With this one line:

self.dispatch(image: image, completion: completion)

Then, replace these lines:

if let responseQueue = self.responseQueue {


responseQueue.async { completion(nil, error) }

} else {
completion(nil, error)
}

With this line:

self.dispatch(error: error, completion: completion)

Build and run the tests to verify they all still pass.

Next, you need to refactor the tests. Specifically, there’s a lot of duplicated code for
verifying that you are dispatching to the responseQueue.

Add this right after whenDownloadImage, towards the top of the file:

// MARK: - Then
func verifyDownloadImageDispatched(image: UIImage? = nil,
error: Error? = nil,
line: UInt = #line) {
mockSession.givenDispatchQueue()
sut = ImageClient(responseQueue: .main,
session: mockSession)

var receivedThread: Thread!


let expectation = self.expectation(
description: "Completion wasn't called")

// when
let dataTask =
sut.downloadImage(fromURL: url) { _, _ in

raywenderlich.com 211
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

receivedThread = Thread.current
expectation.fulfill()
} as! MockURLSessionTask
dataTask.completionHandler(image?.pngData(), nil, error)

// then
waitForExpectations(timeout: 0.2)
XCTAssertTrue(receivedThread.isMainThread, line: line)
}

This code is very similar to how the last two unit tests validate
receivedThread.isMainThread. However, it accepts an image, error and line as
inputs. It uses these to call dataTask.completionHandler and then XCTAssert.

You’ve also duplicated expectedError in a couple of places, so you’ll move this into
a property. Add this line after the other properties:

var expectedError: NSError!

Just like the others, you also need to ensure expectedError is reset after each test
run. Add this line to tearDown right before super.tearDown():

expectedError = nil

You also need a helper method to set expectedError. Add this right after
givenExpectedImage:

func givenExpectedError() {
expectedError = NSError(domain: "com.example",
code: 42,
userInfo: nil)
}

You can now update the unit tests to make use of these methods. First, replace the
line for let expectedError = within
test_downloadImage_givenError_callsCompletionWithError with this:

givenExpectedError()

Next, replace the entire contents of


test_downloadImage_givenImage_dispatchesToResponseQueue with this:

// given
givenExpectedImage()

// then
verifyDownloadImageDispatched(image: expectedImage)

raywenderlich.com 212
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Lastly, replace the contents of


test_downloadImage_givenError_dispatchesToResponseQueue with this:

// given
givenExpectedError()

// then
verifyDownloadImageDispatched(error: expectedError)

Very nice! You’ve greatly simplified these tests by using your helper methods.

Build and run the unit tests to verify they all pass.

Caching
Your ImageClient is really coming along, but it’s still missing a critical piece of
functionality: Caching. Specifically, you need to cache images that the user has
already downloaded.

Add the following test right after the last one:

func test_downloadImage_givenImage_cachesImage() {
// given
givenExpectedImage()

// when
whenDownloadImage(image: expectedImage)

// then
XCTAssertEqual(sut.cachedImageForURL[url]?.pngData(),
expectedImage.pngData())
}

This test asserts that the expected image is cached. Build and run the tests to
validate this fails. To make it pass, add the following right after the if let data =
line within downloadImage on ImageClient:

self.cachedImageForURL[url] = image

Build and run the test again to ensure it passes.

If there’s already a cached image, you don’t want to start another task. Instead, you
should immediately call the completion with it and return nil from
downloadImage.

raywenderlich.com 213
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Add the following test next:

func test_downloadImage_givenCachedImage_returnsNilDataTask() {
// given
givenExpectedImage()

// when
whenDownloadImage(image: expectedImage)
whenDownloadImage(image: expectedImage)

// then
XCTAssertNil(receivedTask)
}

You pass expectedImage into whenDownloadImage, which caches the image. You
then call this method a second time and assert that receivedTask is nil.

Build and run this test to ensure it fails. To make it pass, change the return type for
downloadImage on ImageClient from URLSessionTaskProtocol to
URLSessionTaskProtocol?.

However, this causes a compiler error because ImageClient no longer conforms to


ImageService. Change the return type for downloadImage within ImageService to
URLSessionTaskProtocol? as well.

Then, add these lines to downloadImage on ImageClient, right after the method’s
opening curly brace:

if let image = cachedImageForURL[url] {


return nil
}

You check if an image already exists in the cachedImageForURL; if so, you return nil
for the task.

Build and run the unit tests to verify they all now pass.

If there’s a cached image, you also need to immediately call completion with it. Add
this test to verify this behavior happens:

func
test_downloadImage_givenCachedImage_callsCompletionWithImage() {
// given
givenExpectedImage()

// when
whenDownloadImage(image: expectedImage)
receivedImage = nil

raywenderlich.com 214
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

whenDownloadImage(image: expectedImage)

// then
XCTAssertEqual(expectedImage.pngData(),
receivedImage?.pngData())
}

You call whenDownloadImage with expectedImage and then immediately reset


receivedImage to nil. This ensures receivedImage isn’t set per this first call. You
call whenDownloadImage again with expectedImage and assert receivedImage is
set.

Build and run to ensure this test fails. To make it pass, add the following right after
if let image = cachedImageForURL[url] {:

completion(image, nil)

You immediately execute completion if the image is found in the cache.

Build and run the tests again, and they should all now pass.

Setting an image view from a URL


Remember how you declared another method on ImageService, setImage(on
imageView: fromURL url: withPlaceholder image:)?

You’ll implement this as a convenience method for setting an image on an image


view from a URL. But wait! Can’t you just call
downloadImage(fromURL:completion:) directly?

You could, but you’d need to handle caching logic: What happens if you’re already
downloading an image for the image view? For example, what happens if you’re
displaying the image view in a table view… which is exactly what
ListingsViewController does?

In this case, you’d need to do the following:

1. Cancel the cached task for the image view, if one exists.

2. Set a placeholder image on the image view.

3. Call downloadImage and cache the task for the image view.

4. Remove the cached task for the image view.

raywenderlich.com 215
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

5. Set the downloaded image on the image view.

6. Handle what happens if an error is received.

You now have a plan for implementing setImage(on:fromURL:withPlaceholder:)!

Canceling a cached task


First, add this test to validate that you’ve canceled the existing task when
setImageOnImageView is called, ignoring the compiler error for now:

func test_setImageOnImageView_cancelsExistingDataTask() {
// given
let task = MockURLSessionTask(
completionHandler: { _, _, _ in },
url: url,
queue: nil)
let imageView = UIImageView()
sut.cachedTaskForImageView[imageView] = task

// when
sut.setImage(on: imageView,
fromURL: url,
withPlaceholder: nil)

// then
XCTAssertTrue(task.calledCancel)
}

You create a task and imageView and insert these into


sut.cachedTaskForImageView. You then call setImage and assert that
task.calledCancel is true.

However, you haven’t actually declared cancel() on URLSessionTaskProtocol, so


this is causing a compiler error here. To fix this, open URLSessionProtocol.swift,
and add this to URLSessionTaskProtocol right after its opening curly brace:

func cancel()

You declare cancel() as a required method on this protocol. Because


URLSessionTask already implements cancel(), you don’t need to make any other
changes in its extension. However, you do need to update MockURLSessionTask to
implement this new method.

raywenderlich.com 216
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Open MockURLSession.swift, and add this code to MockURLSessionTask right


before the calledResume property:

var calledCancel = false


func cancel() {
calledCancel = true
}

You add a property for calledCancel with a default value of false, and you set this
to true whenever cancel is called.

Build and run the tests to verify this one fails. To get it to pass, add the following
code within setImage on ImageClient:

cachedTaskForImageView[imageView]?.cancel()

Build and run the tests again, and they’ll now pass.

Setting a placeholder image


Next, add this test to ensure the placeholder image is set on the imageView:

func test_setImageOnImageView_setsPlaceholderOnImageView() {
// given
givenExpectedImage()
let imageView = UIImageView()

// when
sut.setImage(on: imageView,
fromURL: url,
withPlaceholder: expectedImage)

// then
XCTAssertEqual(imageView.image?.pngData(),
expectedImage.pngData())
}

You call givenExpectedImage() to set expectedImage and then create an


imageView. You then call setImage with imageView and expectedImage and then
assert the data for imageView.image equals the data for the expectedImage.

Build and run the tests, and you’ll see this last one fails. To make it pass, you want to
set the image on the imageView to the placeholder. To do it, add this to setImage
on ImageClient, right before the closing method brace:

imageView.image = placeholder

raywenderlich.com 217
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Build and run the tests again to ensure they all pass.

Is there anything to refactor? Yes, you’ve duplicated imageView in two tests. To


eliminate the duplication, add this property after the others in ImageClientTests:

var imageView: UIImageView!

Then, add this line within tearDown to reset imageView after each run, right before
calling super.tearDown():

imageView = nil

While you could create a helper method for givenImageView(), you’ll be using
imageView in several tests. Hence, you’ll set it before each test run instead. Add this
line to setUp(), right before setting sut:

imageView = UIImageView()

Finally, delete each let imageView line to eliminate the duplication.

Caching the download image task


Next, you need to call downloadImage and cache the download task for the image
view. Add this test right after the last one:

func test_setImageOnImageView_cachesTask() {
// when
sut.setImage(on: imageView,
fromURL: url,
withPlaceholder: nil)

// then
receivedTask = sut.cachedTaskForImageView[imageView]
as? MockURLSessionTask
XCTAssertEqual(receivedTask?.url, url)
}

You call sut.setImage, unwrap receivedTask using imageView in


cachedTaskForImageView and then assert dataTask.url equals url.

raywenderlich.com 218
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Build and run the tests to verify this fails. To make it pass, add the following to
setImage on ImageClient, immediately before the method’s closing brace:

cachedTaskForImageView[imageView] =
downloadImage(fromURL: url) { [weak self] image, error in
guard let self = self else { return }

Build and run the tests to verify the last one now passes.

Removing the cached task


When downloadImage completes, you also need to remove task from the cache. Add
this test for this:

func test_setImageOnImageView_onCompletionRemovesCachedTask() {
// given
givenExpectedImage()

// when
sut.setImage(on: imageView,
fromURL: url,
withPlaceholder: nil)
receivedTask = sut.cachedTaskForImageView[imageView]
as? MockURLSessionTask
receivedTask?.completionHandler(
expectedImage.pngData(), nil, nil)

// then
XCTAssertNil(sut.cachedTaskForImageView[imageView])
}

You call setImage and unwrap receivedTask. You then call completionHandler on
receivedTask and finally assert the task is removed from the cache.

Build and run this test to verify it falls. To make it pass, add this line within
setImage, immediately after the guard statement you added before:

self.cachedTaskForImageView[imageView] = nil

This removes the cached task for imageView from cachedTaskForImageView.

Build and run the tests again to verify they all pass.

raywenderlich.com 219
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Setting the image on image view


Lastly, you need to set the downloaded image on the image view. Add this test right
after the last one:

func test_setImageOnImageView_onCompletionSetsImage() {
// given
givenExpectedImage()

// when
sut.setImage(on: imageView,
fromURL: url,
withPlaceholder: nil)
receivedTask = sut.cachedTaskForImageView[imageView]
as? MockURLSessionTask
receivedTask?.completionHandler(
expectedImage.pngData(), nil, nil)

// then
XCTAssertEqual(imageView.image?.pngData(),
expectedImage.pngData())
}

This test is very similar to the last one; the difference is you assert that the image
data from the imageView equals the data from the expectedImage.

Build and run this test, and you’ll see it fails. To make it succeed, add this code
within downloadImage on ImageClient after the previous line you added to set the
image on the imageView:

imageView.image = image

Build and run to verify the test now passes. However, you now have duplicated code
that needs refactoring.

Add the following code right after whenDownloadImage:

func whenSetImage() {
givenExpectedImage()
sut.setImage(on: imageView,
fromURL: url,
withPlaceholder: nil)
receivedTask = sut.cachedTaskForImageView[imageView]
as? MockURLSessionTask
receivedTask?.completionHandler(
expectedImage.pngData(), nil, nil)
}

raywenderlich.com 220
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

You’ve moved the common code into this method. Hence, you’ll need to replace the
contents of test_setImageOnImageView_onCompletionRemovesCachedTask with
the following:

// when
whenSetImage()

// then
XCTAssertNil(sut.cachedTaskForImageView[imageView])

Then, replace the contents of


test_setImageOnImageView_onCompletionSetsImage with this:

// when
whenSetImage()

// then
XCTAssertEqual(imageView.image?.pngData(),
expectedImage.pngData())

Nice! This makes both of these tests much simpler.

Handling a download image error


In the case of an error, you’ll simply not set the image and instead will print a
message to the console. To verify this happens, add the following test next:

func test_setImageOnImageView_givenError_doesnSetImage() {
// given
givenExpectedImage()
givenExpectedError()

// when
sut.setImage(on: imageView,
fromURL: url,
withPlaceholder: expectedImage)
receivedTask = sut.cachedTaskForImageView[imageView]
as? MockURLSessionTask
receivedTask?.completionHandler(nil, nil, expectedError)

// then
XCTAssertEqual(imageView.image?.pngData(),
expectedImage.pngData())
}

raywenderlich.com 221
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Here’s how this works:

• Within given, you call givenExpectedImage() to create expectedImage and


givenExpectedError to create expectedError.

• Within when, you call setImage, unwrap the task and execute its
completionHandler with the expectedError. As a consequence, this sets the
expectedImage on the imageView because it’s passed as the placeholder image.

• Within then, you assert that the image on the imageView is still set to the
expectedImage.

Build and run this test to verify that it fails. To make it pass, replace this line within
downloadImage on ImageClient:

imageView.image = image

With this instead:

guard let image = image else {


print("Set Image failed with error: " +
String(describing: error))
return
}
imageView.image = image

You guard that image is actually set here. If it is not, you print the error to the
console. If it is, you set it on the imageView.

Build and run the test, and they’ll all pass.

Using the image client


Great job implementing the ImageClient! You’re now ready to use it in
ListingsViewController.

Before you do, you need to create a MockNetworkClient. Create a new Swift File in
DogPatchTests/Test Types/Mocks named MockImageService.swift. Replace its
contents with the following:

@testable import DogPatch


import UIKit

// 1
class MockImageService: ImageService {

raywenderlich.com 222
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

// 2
func downloadImage(
fromURL url: URL,
completion: @escaping (UIImage?, Error?) -> Void)
-> URLSessionTaskProtocol? {
return nil
}

// 3
var setImageCallCount = 0
var receivedImageView: UIImageView!
var receivedURL: URL!
var receivedPlaceholder: UIImage!

// 4
func setImage(on imageView: UIImageView,
fromURL url: URL,
withPlaceholder placeholder: UIImage?) {
setImageCallCount += 1
receivedImageView = imageView
receivedURL = url
receivedPlaceholder = placeholder
}
}

Here’s how this works:

1. You create a new MockImageService that conforms to ImageService.

2. You implement downloadImage because MockImageService requires it, but you


won’t actually need it for now. Hence, you simply return nil from it.

3. You declare properties for the setImageCallCount and received values.

4. You implement setImage, per the other method required by MockImageService.


Therein, you increment setImageCallCount and set each of the received
properties.

You can now put this mock to good use! Open ListingsViewControllerTests.swift
and add the following right before // MARK: - View Life Cycle - Tests:

func test_imageClient_isImageService() {
XCTAssertTrue((sut.imageClient as AnyObject) is ImageService)
}

You here cast sut.imageClient as AnyObject to silence a warning and then assert it
is an ImageService. This test doesn’t compile, however, because you haven’t
declared imageClient on ListingsViewController yet.

raywenderlich.com 223
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Add the following property after var networkClient within


ListingsViewController:

var imageClient: ImageService =


ImageClient(responseQueue: nil,
session: URLSession())

Build and run the tests, and the last one should now succeed.

Next, add this test right after test_imageClient_isImageService to ensure that


imageClient is actually set to ImageClient.shared:

func test_imageClient_setToSharedImageClient() {
// given
let expected = ImageClient.shared

// then
XCTAssertTrue((sut.imageClient as? ImageClient) === expected)
}

Build and run this test to ensure it fails. To make it pass, update the var
imageClient declaration in ListingsViewController with the following:

var imageClient: ImageService = ImageClient.shared

Build and run the tests again to ensure they pass.

You next need a MockImageClient to set as the imageClient on ImageService. To


ensure you don’t accidentally make real network calls, you’ll create this within
setUp().

Before you can do this, you first need a new property for mockImageClient. Add this
right before var mockNetworkClient on ListingsViewControllerTests:

var mockImageClient: MockImageService!

Then add the following, right after setting sut within setUp():

mockImageClient = MockImageService()
sut.imageClient = mockImageClient

Add the following line of code to tearDown() to set mockImageClient to nil:

mockImageClient = nil

raywenderlich.com 224
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

If you build and run the tests, however, you’ll see that
test_imageClient_setToSharedImageClient now fails! This is because you set
sut.imageClient within setUp to mockImageClient, and hence, it’s never going to
be equal to ImageClient.shared.

Fortunately, this fix is easy. Add the following line to


test_imageClient_setToSharedImageClient right after // given:

sut = ListingsViewController.instanceFromStoryboard()

Build and run the tests again, and they’ll all pass now.

You’re now ready to use mockImageClient in a unit test. Add this test after the very
last one:

func
test_tableViewCellForRowAt_callsImageClientSetImageWithDogImageV
iew() {
// given
givenMockViewModels()

// when
let indexPath = IndexPath(row: 0, section: 0)
let cell = sut.tableView(sut.tableView,
cellForRowAt: indexPath)
as? ListingTableViewCell

// then
XCTAssertEqual(mockImageClient.receivedImageView,
cell?.dogImageView)
}

• Within given, you call givenMockViewModels() to create an array of view models


and set this on sut.

• Within when, you dequeue the cell for the first IndexPath and cast this to
ListingTableViewCell.

• Within then, you assert that receivedImageView on mockImageClient matches


the dogImageView on the cell.

raywenderlich.com 225
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Build and run this test to verify it fails. To make it pass, you need to actually pass
cell.dogImageView into imageClient.setImage. Add this code within
listingCell(_:_:) on ListingsViewController, right before the return line:

imageClient.setImage(
on: cell.dogImageView,
fromURL: URL(string: "http://example.com")!,
withPlaceholder: nil)

Build and run the tests again, and the last one will now pass.

You also need to ensure that you’re passing the correct URL into
imageClient.setImage. Add this test after the last one:

func
test_tableViewCellForRowAt_callsImageClientSetImageWithURL() {
// given
givenMockViewModels()
let viewModel = sut.viewModels.first!

// when
let indexPath = IndexPath(row: 0, section: 0)
_ = sut.tableView(sut.tableView, cellForRowAt: indexPath)

// then
XCTAssertEqual(mockImageClient.receivedURL,
viewModel.imageURL)
}

Similar to the last test, you first call givenMockViewModels() to set


sut.viewModels and then get the first one from it. You then call
sut.tableView(_:cellForRowAt:) to trigger configuring the first cell and then
assert mockImageClient.receivedURL equals the viewModel.imageURL.

Build and run the tests, and you’ll see this one fails. To make it succeed, replace this
argument within ListingsViewController:

URL(string: "http://example.com")!

With this code:

viewModel.imageURL

This passes the imageURL from viewModel into the imageClient.setImage call.

Build and run the tests to ensure they all pass.

raywenderlich.com 226
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

For the refactor step, you now have similar code in the last two tests. To get rid of the
duplication, add the following method after whenDequeueTableViewCells:

@discardableResult
func whenDequeueFirstListingsCell()
-> ListingTableViewCell? {
let indexPath = IndexPath(row: 0, section: 0)
return sut.tableView(sut.tableView,
cellForRowAt: indexPath)
as? ListingTableViewCell
}

You here dequeue the first table view cell and then cast it as
ListingTableViewCell.

Next, replace the when section in


test_tableViewCellForRowAt_callsImageClientSetImageWithDogImageView
with the following:

// when
let cell = whenDequeueFirstListingsCell()

Then, replace the when section in


test_tableViewCellForRowAt_callsImageClientSetImageWithURL with this:

whenDequeueFirstListingsCell()

Great, that takes care of the duplicated code!

Lastly, you need a test to confirm that you’re passing the placeholder image into
setImage. Add this test after the last one:

func
test_tableViewCellForRowAt_callsImageClientWithPlaceholder() {
// given
givenMockViewModels()
let placeholder = UIImage(named: "image_placeholder")!

// when
whenDequeueFirstListingsCell()

// then
XCTAssertEqual(
mockImageClient.receivedPlaceholder.pngData(),
placeholder.pngData())
}

raywenderlich.com 227
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

This test is similar to the previous ones. However, you here declare an expected
placeholder and assert that the underlying data on
mockImageClient.receivedPlaceholder is the same as it.

Build and run the tests to confirm this one fails. To make it pass, replace the
following in ListingsViewController:

withPlaceholder: nil

With this instead:

withPlaceholder:
UIImage(named: "image_placeholder")

Build and run your tests again and they should all pass.

Now for the fun part – you’ve done TDD to create and even use the ImageClient, but
you haven’t seen your hard work pay off yet. You’re finally ready to use it!

Build and run the app to check and see how ImageClient loads and displays images
onscreen.

raywenderlich.com 228
iOS Test-Driven Development by Tutorials Chapter 10: ImageClient

Key points
In this chapter, you learned how to do TDD for an image client. Here are the key
points:

• You created a service protocol to make mocking easy, just like with a network
client.

• You created downloadImage(...) to handle one-off image download requests and


to cache downloaded images.

• You created setImage(...) to make setting an image from a URL on an image


view more convenient.

• Remember to refactor as you go! For example, you can pull out helper methods
and properties for turning asynchronous “download” calls into synchronous tests.

You’ve created the core functionality for DogPatch and learned a lot about
networking along the way! There’s still more functionality you could add, including

• Authentication

• Messaging

• Ratings

• User preferences

• And much more!

Some of these would require back-end support, but you can add many local features
too. Of course, remember to do TDD for networking and local features alike! :]

Feel free to tinker with DogPatch as much as you’d like. When you’re ready, move
onto the next section to learn about TDD on a legacy app.

raywenderlich.com 229
Section IV: TDD in Legacy
Apps

This section will show you how to start test-driven development in a legacy app that
lacks sufficient unit tests. You’ll learn strategies for introducing TDD into existing
apps, methods for visualizing and splitting up dependencies, ways to add features
safely alongside existing code and how to refactor large classes.

Throughout this section, you’ll introduce TDD into an app for managing a business.
The app is feature-rich with spaghetti code and ready for a TDD clean up!

Several techniques and concepts in this section were inspired by Michael Feather’s
book Working Effectively with Legacy Code. Reading that book isn’t a strict
requirement for working through these chapters. However, you’ll likely benefit by
having some familiarity with the topics herein if you already have read it!

raywenderlich.com 230
11 Chapter 11: Legacy
Problems
By Michael Katz

Beginning TDD on a “legacy” project is much different than starting TDD on a new
project. For example, the project may have few (if any) unit tests, lack
documentation and be slow to build. This chapter will introduce you to strategies for
tackling these problems.

You may think, “If only this project were created using TDD, it wouldn’t be this bad.”
Making the code more testable while adding unit tests is a great way to address these
issues. Unfortunately, there isn’t a silver-bullet, sure-fire way to fix all of these issues
overnight.

However, there are great strategies you can use to introduce TDD to legacy projects
over time. In this chapter, you’ll be introduced to the Legacy Code Change
Algorithm, which was originally introduced by Michael Feathers in his book Working
Effectively with Legacy Code. Here are the high-level steps:

1. Identify change points

2. Find test points

3. Break dependencies

4. Write tests

5. Make changes and refactor

raywenderlich.com 231
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Introducing MyBiz
MyBiz is the sample app for this section. It’s a very lightweight ERP app but will be
illustrative of the kinds of issues you may encounter working with legacy apps. Don’t
worry if ERP is a meaningless acronym to you. It stands for Enterprise Resource
Planning, which is a four-dollar expression for “kitchen sink of business crap.”

In our TDD-world, “legacy app” most importantly means an app without adequate
(or any) unit tests. And if “legacy” means code without any tests, then this app is
capital-L Legacy.

Bloated, convoluted apps are common in large enterprises, such as where MyBiz
would be used; however, these issues occur in all kinds of apps in organizations of
different sizes and maturities. As soon as that first feature is added to an app that
wasn’t architected to support it, these “legacy (anti-) patterns” start cropping up.
Introducing TDD in your legacy app while adding features is a great way to avoid
this.

One challenge working with MyBiz is that it does not use a modern architecture like
MVVM or VIPER. Instead, a lot of the business logic exists in monolithic view
controllers. It gets the job done, but, as you’ll see, it’s hard to add new things.

raywenderlich.com 232
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Setting up the app and backend


Before launching the starter app, you should fire up the backend. Like the Dogpatch
app in Section 3, this is a Vapor-based backend. It’s very barebones for an ERP app,
which would normally talk to a big multi-tiered services architecture made up of
multiple servers and databases. However, the goal of this project is just to have a
functional app for adding features, tests and refactoring, so the backend is high level
and abstract.

Follow the installation instructions found in Chapter 8, “RESTful Networking,” to


install Vapor.

Note: To learn more about Vapor, you can read the documentation at https://
vapor.codes/ or check out our book Server-Side Swift With Vapor, which you can
find at https://www.raywenderlich.com/books/server-side-swift-with-vapor.

Once Vapor is installed, fire up the backend by doing the following:

1. Open a Terminal and navigate to the projects/backend folder.

2. Run the following command to create your project file and open the Xcode
project.

vapor xcode -y

3. Set the scheme to Run if it is not selected already.

4. Build and run.

You should see the terminal pop up at the bottom of the screen with the following
text:

Server starting on http://localhost:8080

This means that the server is up and running. To check it out, open your web browser
and visit localhost:8080/hello. You should see the following:

Welcome to MyBiz!

raywenderlich.com 233
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

With the backend ready to go, open the starter project. Build and run. Click “Sign in”
to use the hard coded credentials: username agent@shield.org and password
"hailHydra". If the backend is set up properly, you’ll see a few tabs filled out with
sample data.

Introducing the change task


To boost morale, the MyBiz HR Director has instituted a new policy of recognizing
employee birthdays. As part of this process, you’ve been directed to add birthdays as
events in the company calendar. For simplicity, assume that every user wants to see
everyone else’s birthday.

raywenderlich.com 234
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Identifying a change point


To change an app, you must figure out where to put that change – that is, figure out
which classes and files need to be modified. The first step is understanding the
requirements so you know exactly what to implement.

You can distill the HR Director’s ask into the following statement:

Populate the user calendar with birthday events, one for each person in the
organization’s contact book.

There are a lot of ways this can be done. For this tutorial, you’ll take the following
approach:

• Add a birthday field for each employee.

• For each employee, add a birthday event to the calendar.

From the above, the change points are:

• Employee.swift: You’ll add a birthdate field.

• CalendarViewController.swift: You’ll need to add birthdays to the events list.

Finding a test point


Test points are the locations where you need to write tests to support your changes.
Test points aren’t about fixing bugs, they are to preserve existing app behavior. Just
as the TDD process isn’t about finding bugs, it instead prevents bugs later on as
changes are introduced.

For legacy code, you’ll write characterization tests. These are tests that make
explicit the current behavior of the code based on what the code does. With a big
legacy app, especially in an enterprise, it’s important to understand and preserve the
code’s behavior – ever hear the phrase, “That’s not a bug, that’s a feature”? The
current users expect the app to behave a certain way, even if it isn’t what’s intended
by the product manager, or what was written out in the spec.

Characterization tests are written for the code you plan to change and for that
change’s broader context (such as its class or callers). If the change includes moving
code or refactoring code, these tests should cover that code as well.

raywenderlich.com 235
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

There’s a TDD-like formula for writing a characterization test. It’s a little like TDD
except the code is already written:

1. Use the code in a test function.

2. Write an assertion that you expect to fail.

3. Let the failure characterize the behavior.

4. Change the test so that it passes based on the code’s behavior.

The main difference from TDD is in the last step above. You’ll change the test to
match the code, rather than change the code to pass the test.

To better understand, you’ll apply this to a specific example.

Your test point will be in CalendarViewController, which is currently responsible


for loading the list of events. You need to write characterization tests regarding the
loading and displaying of events in the calendar so that adding birthdays does not
break the app.

Using the code in a test


First, you’ll need a place to put those characterization tests. To do that, create a new
test target:

1. Add a new iOS Unit Testing Bundle target to the project. Name it
CharacterizationTests.

raywenderlich.com 236
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

A separate unit test target will be used for TDD-based unit tests as you add new
code. It’s not necessary to separate characterization tests from other tests by a
target, but, this way, you’ll have a clear idea of what the goals of these tests are.

2. Delete the CharacterizationTests.swift stub file.

3. Add a new group: Cases.

4. In that group, add a new Unit Test Case Class, named


CalendarViewControllerTests.

5. Remove testExample and testPerformanceExample.

When it’s done, the CalendarViewControllerTests group should look like this:

Now, it’s time to set this class up for your tests.

First, add the app module import to the top of the file:

@testable import MyBiz

Next, add the following at the top of the class:

var sut: CalendarViewController!

Finally, replace setUpWithError() and tearDownWithError() with the following:

override func setUpWithError() throws {


try super.setUpWithError()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "Calendar") as?
CalendarViewController
sut.loadViewIfNeeded()
}

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}

raywenderlich.com 237
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

You’ve set up CalendarViewController as your System Under Test (SUT) and


you’ve loaded the view. Now you’re ready to write a test… but what will it be?

Breaking dependencies
A logical place to start is where events are loaded into the calendar. If you add
birthdays to the list of events, you want to make sure not to break the existing event
functionality.

Add the following test method at the end of the class:

func testLoadEvents_getsData() {

The next step is to have the view controller load events, but if you look in
CalendarViewController, you’ll notice this is done by a call made in
viewWillAppear(_:). This method is hard to test since that would mean performing
view lifecycle events and dealing with unknown side effects.

To make testing easier, refactor the view controller so that loading events don’t
require calling viewWillAppear(_:). Select the last two lines of
viewWillAppear(_:) in CalendarViewController.swift. Then, select Editor ▸
Refactor ▸ Extract to Method. Name this new method loadEvents. Delete the
fileprivate modifier so that your tests can access this method.

Now, events can be loaded in the test class. Open


CalendarViewControllerTests.swift, add the following to
testLoadEvents_getsData:

// when
sut.loadEvents()

This kicks off the events load, but you’re not yet ready to confirm if the data loaded.

raywenderlich.com 238
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Next, add the following to the end of the test:

let predicate = NSPredicate { _, _ -> Bool in


return !self.sut.events.isEmpty
}
let exp = expectation(
for: predicate,
evaluatedWith: sut,
handler: nil)

// then
wait(for: [exp], timeout: 2)
print(sut.events)

This waits for the events to load and then prints them out to the console.

Making the characterization into a test


This is not yet a true test since there is no assert, but this is a crucial step for
characterizing the system as is.

Build and test testLoadEvents_getsData(), then take a look at the console. You
should see something similar to the following:

[MyBiz.Event(name: "Alien invasion", date: 2021-11-05 12:00:00


+0000, type: MyBiz.EventType.appointment, duration: 3600.0),
MyBiz.Event(name: "Interview with Hydra", date: 2021-11-05
17:30:00 +0000, type: MyBiz.EventType.appointment, duration:
1800.0), MyBiz.Event(name: "Panic attack", date: 2021-11-12
15:00:00 +0000, type: MyBiz.EventType.meeting, duration:
3600.0)]

You can use these results to write the expectation.

Replace the print() in your test with the following. Update the dates to match the
values you saw in the console — with some extra formatting. For each date copied
from the console, replace the space between the date and time with a T, and remove
the space between the time and timezone:

let eventJson = """


[{"name": "Alien invasion", "date":
"2021-11-05T12:00:00+0000",
"type": "Appointment", "duration": 3600.0},
{"name": "Interview with Hydra", "date":
"2021-11-05T17:30:00+0000",
"type": "Appointment", "duration": 1800.0},
{"name": "Panic attack", "date": "2021-11-12T15:00:00+0000",
"type": "Meeting", "duration": 3600.0}]

raywenderlich.com 239
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

"""
let data = Data(eventJson.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let expectedEvents = try? decoder.decode([Event].self, from:
data)
XCTAssertEqual(sut.events, expectedEvents)

Here, you’ve hard-coded an events JSON payload and decoded it. The assert validates
that your payload matches the one in sut.events, which was loaded by
loadEvents().

Note: The actual date will differ since the sample backend is coded to return
events relative to your current date. This points out an actual problem you’ll
experience connecting to a “live” backend — the data may change and make
your tests unreliable. Fortunately, you won’t stay in this zone for long.

Now, run the test and it will still pass, but this time with an actual assert.

Adding a little stability


This is a good start but, as mentioned above, this test has a brittle dependency on
the backend. Just wait a day and this test will no longer pass.

To work around this instability, you need to break dependencies until the test no
longer depends on live API calls. “Restful Networking,” covers the theories and
strategies for how to do this. In this next step, you’ll do a light version of that using a
mock that overrides production code. This way you will to be able to proceed on the
original goal: adding birthdays.

Get started by modifying CalendarViewController to support a Mock API class.

In CalendarViewController.swift, replace the var api line with:

var api: API = UIApplication.appDelegate.api

This subtle change from a computed variable to a stored one will allow you to
replace it in the test. You should re-run the test to verify that this change did not
break any of the characterized behavior.

In the CharacterizationTests group, create a new group: Mocks. Inside, create a


new Swift File, named MockAPI.swift.

raywenderlich.com 240
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

When you’re done, the CharacterizationTests group will look like this:

Add the following code to the new file:

@testable import MyBiz

class MockAPI: API {


var mockEvents: [Event] = []

override func getEvents() {


DispatchQueue.main.async {
self.delegate?.eventsLoaded(events: self.mockEvents)
}
}
}

MyBiz uses the API class to communicate with its backend. Here, you’ve created an
API subclass that overrides getEvents(), calling eventsLoaded(events:) with
mock data rather than making a service call. This is a baby step towards refactoring
out the networking calls to make stable tests that can cover a range of cases.

Now, use it in CalendarViewControllerTests.swift. Add a var:

var mockAPI: MockAPI!

Create it by adding these just before the loadViewIfNeeded line in


setUpWithError:

mockAPI = MockAPI()
sut.api = mockAPI

And to tearDownWithError, above the call to super:

mockAPI = nil

raywenderlich.com 241
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Finally, rewrite testLoadEvents_getsData as follows:

func testLoadEvents_getsData() {
// given
let eventJson = """
[{"name": "Alien invasion", "date":
"2019-04-10T12:00:00+0000",
"type": "Appointment", "duration": 3600.0},
{"name": "Interview with Hydra", "date":
"2019-04-10T17:30:00+0000",
"type": "Appointment", "duration": 1800.0},
{"name": "Panic attack", "date":
"2019-04-17T14:00:00+0000",
"type": "Meeting", "duration": 3600.0}]
"""
let data = Data(eventJson.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let expectedEvents = try! decoder.decode([Event].self, from:
data)

mockAPI.mockEvents = expectedEvents

// when
let predicate = NSPredicate { _, _ -> Bool in
!self.sut.events.isEmpty
}
let exp = expectation(for: predicate, evaluatedWith: sut,
handler: nil)

sut.loadEvents()

// then
wait(for: [exp], timeout: 1)
XCTAssertEqual(sut.events, expectedEvents)
}

This uses expectedEvents, loaded from hard coded data, to seed the mockAPI. It
then tests that those values come back out when the events are loaded. Now, there is
no more worry about the date of running the test. Run the test, and you should see it
pass, regardless of what day you run it. That’s because the data is frozen forever in
the test JSON.

Over the next few chapters, you’ll further refactor the API class so that the Mock can
implement a protocol rather than override the production code. And then the final
step would be to break up the API protocol into smaller, functional protocols so each
screen only needs to be concerned with its piece.

raywenderlich.com 242
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

It’s important to remember that the goal with this characterization test is not to
ensure correctness, but rather to document what the code actually does. This way,
you’ll be able to identify when later changes modify behavior.

If something unexpected is discovered, it doesn’t necessarily indicate a bug. Instead,


this is an opportunity to get clarification on the intended behavior. If a fix is
required, it can now be done with a test already in place to guide the way.

Having tests like this one in place provides confidence that subsequent refactors will
preserve the app’s behavior. Generally, you’ll want to characterize a little more
behavior than this before making changes — for example, capturing error and
boundary conditions.

Writing tests
Now, it’s time to add the birthday feature. Since this will be new code, you’ll use TDD
to make sure there are tests in place and use those tests to guide your code.

Next, you’ll create a new test target.

1. Add a new iOS Unit Testing Bundle target to the project. Name it MyBizTests.
This target will be for TDD-style tests that cover the new code.

2. Delete MyBizTests.swift.

3. Add a new group: Cases.

4. In that group, add a new Unit Test Case Class, named CalendarModelTests.

raywenderlich.com 243
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

To improve the readability, stability and testability of the codebase while also adding
new features, you’ll create a model class that extracts the data logic out of the view
controller; this will be done with a new class, CalendarModel.

Replace the contents of CalendarModelTests.swift with:

import XCTest
@testable import MyBiz

class CalendarModelTests: XCTestCase {


var sut: CalendarModel!

override func setUpWithError() throws {


try super.setUpWithError()
sut = CalendarModel()
}

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}
}

This uses CalendarModel as the SUT, and you’ll get compile errors since it doesn’t
yet exist.

In the project navigator, select CalendarViewController.swift and


CalendarCell.swift. Create a new group named Calendar by using File ▸ New ▸
Group From Selection.

Add a new swift file named CalendarModel.swift to this group and replace its
contents with the following:

class CalendarModel {
init() {}
}

Now, CalendarModelTests will compile, even if it doesn’t do anything yet.

Start with a basic piece — calculating birthday events from the employee list.

raywenderlich.com 244
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Add the following code to CalendarModelTests:

func mockEmployees() -> [Employee] {


let employees = [
Employee(
id: "Cap",
givenName: "Steve",
familyName: "Rogers",
location: "Brooklyn",
manager: nil,
directReports: [],
birthday: "07-04-1920"),
Employee(
id: "Surfer",
givenName: "Norrin",
familyName: "Radd",
location: "Zenn-La",
manager: nil,
directReports: [],
birthday: "03-01-1966"),
Employee(
id: "Wasp",
givenName: "Hope",
familyName: "van Dyne",
location: "San Francisco",
manager: nil,
directReports: [],
birthday: "01-02-1979")
]
return employees
}

func mockBirthdayEvents() -> [Event] {


let dateFormatter = DateFormatter()
dateFormatter.dateFormat = Employee.birthdayFormat
return [
Event(
name: "Steve Rogers Birthday",
date: dateFormatter.date(from: "07-04-1920")!.next()!,
type: .birthday,
duration: 0),
Event(
name: "Norrin Radd Birthday",
date: dateFormatter.date(from: "03-01-1966")!.next()!,
type: .birthday,
duration: 0),
Event(
name: "Hope van Dyne Birthday",
date: dateFormatter.date(from: "01-02-1979")!.next()!,
type: .birthday,
duration: 0)
]
}

raywenderlich.com 245
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

func testModel_whenGivenEmployeeList_generatesBirthdayEvents() {
// given
let employees = mockEmployees()

// when
let events = sut.convertBirthdays(employees)

// then
let expectedEvents = mockBirthdayEvents()
XCTAssertEqual(events, expectedEvents)
}

mockEmployees() and mockBirthdayEvents() are helpers that create mock data


objects with hardcoded data. These methods will be used in several tests. The new
test confirms that given a list of employees, a correct set of birthday events is
generated.

You’ll need to add code to get this to compile. In Employee.swift, add the following
below let directReports: [String]:

let birthday: String?


static let birthdayFormat = "MM-dd-yyyy"

This adds birthday as a data field and a description of the expected date format. For
this exercise, you can safely assume this format is an iron-clad contract.

Next, add birthday as an event in Event.swift.

1. Add the following to the EventType case list:

case birthday = "Birthday"

This needs the specified raw value to bridge between the lower-case enum
convention and the upper-case server convention.

2. Add the following to the switch in var symbol:

case .birthday:
return "! "

This will be used in populating the title of the birthday event in the calendar detail
view.

raywenderlich.com 246
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Finally in CalendarModel.swift add this method:

func convertBirthdays(_ employees: [Employee]) -> [Event] {


let dateFormatter = DateFormatter()
dateFormatter.dateFormat = Employee.birthdayFormat
return employees.compactMap {
if let dayString = $0.birthday,
let day = dateFormatter.date(from: dayString),
let nextBirthday = day.next() {
let title = $0.displayName + " Birthday"
return Event(
name: title,
date: nextBirthday,
type: .birthday,
duration: 0)
}
return nil
}
}

This method takes an array of employees and returns corresponding events for their
upcoming birthdays.

Now, run CalendarModelTests, and the test will pass. Phew…

Loading birthdays in production


You’re now able to create Events from employee birthdays, but you don’t yet have a
way to load birthdays in production code. You’ll work on that next.

Still in CalendarModelTests.swift, add the following test to the end of the class:

func testModel_whenBirthdaysLoaded_getsBirthdayEvents() {
// given
let exp = expectation(description: "birthdays loaded")

// when
var loadedEvents: [Event]?
sut.getBirthdays { res in
loadedEvents = try? res.get()
exp.fulfill()
}

// then
wait(for: [exp], timeout: 1)
let expectedEvents = mockBirthdayEvents()
XCTAssertEqual(loadedEvents, expectedEvents)
}

raywenderlich.com 247
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

You call a new method, getBirthdays(completion:) that accepts a completion


closure that returns an array of Events.

To get the test to build, add the following to CalendarModel.swift:

func getBirthdays(
completion: @escaping (Result<[Event], Error>) -> Void) {
}

But to get it to pass, you’ll need to build out some API-based functionality.

Add the following to CalendarModel above convertBirthdays(_:):

let api: API


var birthdayCallback: ((Result<[Event], Error>) -> Void)?

init(api: API) {
self.api = api
}

Also, delete parameterless init() method, since there is no default value for API.

This makes it possible to inject an API object, which will be used to fetch data from
the server. There is also a variable to store a callback that you’ll use next.

Add the following contents to getBirthdays(completion:):

birthdayCallback = completion
api.delegate = self
api.getOrgChart()

This stores that completion block and calls into the api to get the employee list.

Next, add the following delegate extension at the bottom of the file:

extension CalendarModel: APIDelegate {


func orgLoaded(org: [Employee]) {
let birthdays = convertBirthdays(org)
birthdayCallback?(.success(birthdays))
birthdayCallback = nil
}

func orgFailed(error: Error) {


// TBD - use the callback with an failure result
}

func eventsLoaded(events: [Event]) {}


func eventsFailed(error: Error) {}
func loginFailed(error: Error) {}

raywenderlich.com 248
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

func loginSucceeded(userId: String) {}


func announcementsFailed(error: Error) {}
func announcementsLoaded(announcements: [Announcement]) {}
func productsLoaded(products: [Product]) {}
func productsFailed(error: Error) {}
func purchasesLoaded(purchases: [PurchaseOrder]) {}
func purchasesFailed(error: Error) {}
func userLoaded(user: UserInfo) {}
func userFailed(error: Error) {}
}

orgLoaded(org:) converts the employees to birthday events via


convertBirthdays() and forwards them back to the completion block. It’s called by
getOrgChart() in API on successful completion of the network request. The
remaining stubbed out methods are required by APIDelegate, but won’t be used
here.

You don’t want to rely on this network request for your test. Go back to the test and
use a mock API.

Open CalendarModelTests.swift and add the following above var sut:


CalendarModel!:

var mockAPI: MockAPI!

You’ll see the following compile error:

Use of undeclared type 'MockAPI'

To fix this, open MockAPI.swift, and add it to both test targets in the file inspector:

In setUpWithError() replace:

sut = CalendarModel()

With the following:

mockAPI = MockAPI()
sut = CalendarModel(api: mockAPI)

raywenderlich.com 249
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Next, add the following to tearDownWithError(), before the call to super:

mockAPI = nil

Next, in MockAPI.swift add the following to MockAPI:

// MARK: - Org
var mockEmployees: [Employee] = []

override func getOrgChart() {


DispatchQueue.main.async {
self.delegate?.orgLoaded(org: self.mockEmployees)
}
}

Now, your MockAPI will simply call orgLoaded(org:) returning mockEmployees


when getOrgChart is called.

Finally, open CalendarModelTests.swift and add the following to the given section
of testModel_whenBirthdaysLoaded_getsBirthdayEvents():

mockAPI.mockEmployees = mockEmployees()

This passes the mockEmployees you defined to the MockAPI, so they’ll be returned.
Build and test, and now the test will pass!

Now, you’ve added new code using TDD. This also reuses some of the
characterization test code, which was the mock that was used to break the API
dependency for testing. This is the dual focus of working with legacy code: focus
shifting between adding new code and characterizing and refactoring existing code.

Making a change and refactoring


The final piece in adding the birthday feature is to refactor the view controller to use
the new model and put the birthdays into the calendar view.

To do that, you’ll need to pull the events functionality into the model class.

Start with a test! Add to CalendarModelTests.swift:

func testModel_whenEventsLoaded_getsEvents() {
// given
let expectedEvents = mockEvents()
mockAPI.mockEvents = expectedEvents
let exp = expectation(description: "events loaded")

raywenderlich.com 250
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

// when
var loadedEvents: [Event]?
sut.getEvents { res in
loadedEvents = try? res.get()
exp.fulfill()
}

// then
wait(for: [exp], timeout: 1)
XCTAssertEqual(loadedEvents, expectedEvents)
}

This tests that the main events are loaded into the model as well. There are a few
steps to get this to work.

First, add this helper function to MockAPI.swift, outside of the class, so it can be
easily reused later.

func mockEvents() -> [Event] {


let events = [
Event(
name: "Event 1",
date: Date(),
type: .appointment,
duration: .hours(1)),
Event(
name: "Event 2",
date: Date(timeIntervalSinceNow: .days(20)),
type: .meeting,
duration: .minutes(30)),
Event(
name: "Event 3",
date: Date(timeIntervalSinceNow: -.days(1)),
type: .domesticHoliday,
duration: .days(1))
]
return events
}

This is a static set of test events that can be used when events are needed.

Next, you need to parallel the work you did for getBirthdays(completion:) in
CalendarModel for regular events.

raywenderlich.com 251
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Open CalendarModel.swift and add:

var eventsCallback: ((Result<[Event], Error>) -> Void)?

func getEvents(
completion: @escaping (Result<[Event], Error>) -> Void) {

eventsCallback = completion
api.delegate = self
api.getEvents()
}

This stores a callback block and uses the api class to get the events.

Next, still in CalendarModel.swift, update eventsLoaded(events:) in


APIDelegate to the following:

func eventsLoaded(events: [Event]) {


eventsCallback?(.success(events))
eventsCallback = nil
}

This forwards the events from the API on to the eventsCallback.

Now, the model tests will pass, and you’re halfway there. The next step is to update
the view controller with the new model methods.

Updating the view controller


To help with writing more tests, move mockBirthdayEvents and mockEmployees
from CalendarModelTests.swift to MockAPI.swift (outside the class below
mockEvents()) so they can be re-used in multiple files.

Next create a new Unit Test Case Class, named CalendarViewControllerTests in


MyBizTests. This will be the home for unit tests for new functionality of the view
controller.

Up top, add the following:

@testable import MyBiz

raywenderlich.com 252
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Next, replace the contents of CalendarViewControllerTests with:

var sut: CalendarViewController!


var mockAPI: MockAPI!

override func setUpWithError() throws {


super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "Calendar")
as? CalendarViewController

mockAPI = MockAPI()
sut.api = mockAPI
sut.loadViewIfNeeded()
}

override func tearDownWithError() throws {


mockAPI = nil
sut = nil
super.tearDown()
}

func testLoadEvents_getsBirthdays () {
// given
mockAPI.mockEmployees = mockEmployees()
let expectedEvents = mockBirthdayEvents()

// when
let predicate = NSPredicate { _, _ -> Bool in
!self.sut.events.isEmpty
}
let exp = expectation(
for: predicate,
evaluatedWith: sut,
handler: nil)

sut.loadEvents()

// then
wait(for: [exp], timeout: 1)
XCTAssertEqual(sut.events, expectedEvents)
}

This is very similar to the characterization test class for this controller, except that
this has a test case for loading birthday events.

Finally, open CalendarViewController.swift, and adding the following var:

var model: CalendarModel!

raywenderlich.com 253
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Next, add the following at the end of viewDidLoad :

model = CalendarModel(api: api)

Finally, replace loadEvents with the following:

func loadEvents() {
events = []
model.getBirthdays { res in
if let newEvents = try? res.get() {
self.events.append(contentsOf: newEvents)
self.calendarView.reloadData()
}
}
model.getEvents { res in
if let newEvents = try? res.get() {
self.events.append(contentsOf: newEvents)
self.calendarView.reloadData()
}
}
}

Here, you call getBirthdays(completion:) and getEvents(completion:) on the


model and update the calendarView with the new data on completion.

Finally, you can delete the APIDelegate extension, as the view controller is no
longer the API delegate.

Now, build and test again, and all should pass. Congratulations! You’ve added
employee birthdays to the app’s calendar without breaking anything. The HR
director will be so happy.

raywenderlich.com 254
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Challenges
The next few chapters will cover these types of changes in greater detail, so the
challenge here is pretty light:

Challenge 1: Add error handling


Go back and add error handling for the CalendarViewController. As a hint, you’ll
need a way to mock API errors and handle them in the CalendarModel as well as the
view controller.

Challenge 2: Clean up the code


Clean up the code and make it a little more reliable if there was a single call to the
model for loading the events, instead of two.

Key points
In this chapter, you added a “small” feature of placing calendar events for employee
birthdays following the code change algorithm. Here are the key points:

• Characterization tests let you discover the existing behavior and ensure that the
behavior doesn’t break without warning.

• Test-driven development is then used to laser-focus on the code that needs to be


added or changed to incorporate the new feature.

• Don’t change any more code than you have to without writing tests first.

• You can break dependencies for testing through code injection.

raywenderlich.com 255
iOS Test-Driven Development by Tutorials Chapter 11: Legacy Problems

Where to go from here?


This chapter’s concepts are laid out in the Working Effectively with Legacy Code by
Michael Feathers, which is a helpful read if you want to learn more of the motivating
theory.

The rest of the chapters in this section expand upon these ideas, giving more
specifics and details about the sticking points when applying the code change
algorithm. Chapter 12, “Dependency Maps,” covers dependency mapping, Chapter
13, “Breaking Up Dependencies,” covers modularization and refactoring code
architecture, Chapter 14, “Modularizing Dependencies”, and Chapter 15, “Adding
Features to Existing Large Classes” are dedicated to making big changes.

It’s also helpful if you skipped the last section on networking to go back over and
skim it. Refactoring a poorly architected, backend-heavy application like MyBiz will
require testing and moving code that calls into the networking layer.

raywenderlich.com 256
12 Chapter 12: Dependency
Maps
By Joshua Greene & Michael Katz

Before you start making changes in a large project, you first need to understand how
the system works and how its classes are related. This chapter will help you visualize
this using a tool called dependency maps. You’ll learn:

• What is a dependency map?

• How can you use it to understand complex systems?

• How can you use it to identify problematic relationships?

• How can you use it to break up a complex system into modules?

Feel free to continue using your project from the last chapter, or start fresh from this
chapter’s starter project. For the best hands-on experience, you’ll need a pencil, red
marker, green marker and paper. This is going to get… analog!

Alternatively, a drawing program — or even Keynote — will work too.

raywenderlich.com 257
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Getting started
You may be wondering, “What exactly is a dependency map?” Great question!

Dependency maps are a way to illustrate dependencies between types. Its primary
purpose is to help you understand how a change will affect an entire system. You can
use dependency maps to identify change points, test points and places where you
can pull out types to make your app more modular.

Before making a code change, your first step is to identify what the new behavior
should be. In this case, your job is to move MyBiz’s login functionality into a separate
module. Long-term, the plan is to use the login module in multiple apps.

Moving login into a separate module also has side benefits: Faster incremental
compile times, separation of unit tests and more.

Wouldn’t it be awesome if you could simply move LoginViewController and related


types into a new module and have it just work? Unfortunately, real-world apps aren’t
usually so well architected…!

Consequently, you’ll need to break up dependencies to make this possible. This is the
perfect problem a dependency map can help you solve.

Choosing where to begin


Choosing the “right” place to begin can be a daunting task in a large app.
Fortunately, creating a dependency map is a journey of discovery and you can
iteratively refine it. An educated guess for a starting point is good enough.

Since this is about login, the LoginViewController is a good starting point. Open
MyBiz.xcodeproj and select LoginViewController.swift from the File Hierarchy.

Do you have that pencil and paper handy? (Or a drawing program?) Write
LoginViewController inside a box in the middle of the paper like this:

And there you go, you have a starting point!

raywenderlich.com 258
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Finding direct dependencies


The next step is to identify the type’s direct dependencies.

This was very easy to do when iOS apps were written in Objective-C: You’d simply
look at which files were imported. Swift is trickier because it automatically imports
types within the same module and an iOS app itself is a module. Consequently, you
need to actually scroll through a type and see which types are used to determine its
direct dependencies.

Open LoginViewController.swift and you’ll see the first line is import UIKit. This
isn’t surprising because it’s a UIViewController, after all. Because system libraries
are readily imported anywhere, you can skip adding this to your dependency map.

The next dependency you’ll find comes from the api property, which is of type API
and is accessed directly on the AppDelegate. This is interesting and should be added
to your diagram. Do the following:

1. Draw a box directly above LoginViewController and write AppDelegate within


it.

2. Draw an arrow from the LoginViewController box pointing at the AppDelegate


box. This indicates LoginViewController has a dependency on AppDelegate.

3. Draw another box to the right of AppDelegate and write API within it.

4. Draw an arrow from the AppDelegate box pointing at the API box. This
indicates AppDelegate depends on API. Even if AppDelegate didn’t actually call
any methods or properties on API, it depends on it simply by having a reference
to it.

5. Lastly, draw an arrow from LoginViewController pointing at API to indicate it


also depends on it.

The next dependency is Skin, a helper object for styling the view. Add another box
for Skin to the left of LoginViewController and draw an arrow from
LoginViewController pointing at Skin.

raywenderlich.com 259
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Your diagram should now look like this:

There aren’t any dependencies within viewDidLoad. So, scroll past it and down to
signIn(_:). This method is tricky because its dependencies aren’t explicitly shown.
Rather, the computed properties isEmail and isValidPassword are defined in
Validators.swift and showAlert(title:subtitle:type:skin:) is defined in
UIViewController+Alert.swift.

Draw a new box for Validators to the bottom left of LoginViewController and draw
another box for UIViewController+Alert directly below LoginViewController.
Then, draw one arrow from LoginViewController pointing at Validators and
another arrow pointing at UIViewController+Alert.

Your diagram should now look like this:

Dependency maps show how your code interrelates — the types don’t need to be
classes. Rather, they can be protocols, extensions, files, libraries or anything else that
makes sense for your use case.

raywenderlich.com 260
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

It’s also important to note that dependency maps do not use UML, Archimate or any
other formal specification. Rather, the arrows only indicate that one type depends on
another.

Continue scrolling down and you’ll see LoginViewController conforms to


APIDelegate via an extension. Draw a box for this to the right of API and write
APIDelegate within it. Then, draw an arrow from LoginViewController pointing at
APIDelegate.

There are several models used in this extension: Event, Employee, Announcement,
Product, PurchaseOrder and UserInfo.

You have a few choices for how you represent this on your dependency map:

1. Draw a separate box for each type. This has the advantage of clearly representing
each type, but it takes up more space. This is a good option if the models have a
complex relationship. For example, dependencies on other types, circular
dependencies on the view controller, etc.

2. Draw a single box for Models. This has the advantage of taking up the least
amount of space, but it doesn’t clearly define which models are used. This is a
good option if the models are simple, don’t have complex relationships and it’s
not important for your use case to show exactly which models are used.

3. Draw a single box for Models and list each within it. This is a tradeoff between
the two options above. It minimizes space but also still clearly defines which
models are used. This is a good option if the models don’t have complex
relationships but you still want to clearly show which models are used.

In this app, the models don’t have complex relationships. However, it’s a code smell
that LoginViewController depends on so many models and you should clearly
show this on the diagram. Hence, let’s go with the last option.

Draw another box for Models to the bottom left of LoginViewController and list
each type within it. Then, draw an arrow from LoginViewController pointing at
Models.

raywenderlich.com 261
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Your dependency map should now look like this:

Fantastic! You’ve reached the end of LoginViewController and you’ve identified all
of its direct dependencies. However, its dependencies also have dependencies
themselves. These are so-called “secondary dependencies” of
LoginViewController.

Finding secondary dependencies


Your dependency map looks nice right now with all of the arrows pointing away from
LoginViewController. However, this is because you’ve only inspected
LoginViewController and not any other classes yet.

The next step is to identify secondary dependencies of LoginViewController. This


will give you a better idea of how making a change to LoginViewController might
have ripple effects on other classes.

In particular, the semi-circle between LoginViewController, AppDelegate and API


is very suspicious and warrants further investigation.

Open AppDelegate.swift and repeat the investigation you did for


LoginViewController.

raywenderlich.com 262
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

The first interesting dependency you’ll find is Configuration. Draw a new box for
Configuration above AppDelegate and draw an arrow from AppDelegate pointing
at it.

AppDelegate also has a dependency on API but you identified this earlier and
already have it drawn on the map.

Within showLogin, you’ll see AppDelegate also has a dependency on


LoginViewController. Draw an arrow pointing from AppDelegate to
LoginViewController to indicate this.

Your diagram should now look like this:

Uh oh! you’ve discovered a dependency cycle! AppDelegate and


LoginViewController mutually depend on each other. This may not be causing
problems right now but it’s definitely a code smell and could cause issues in the
future. You’ll deal with this in the next chapter.

That’s it for the dependencies of AppDelegate. So next, open API.swift.

raywenderlich.com 263
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

This file starts by defining APIDelegate. Since its methods use all of the previously-
identified models, it depends on them. Draw an arrow from APIDelegate to Models
to show this.

Even though APIDelegate is defined within the same file as API, this doesn’t
actually make API depend on APIDelegate. If needed, you could easily move
APIDelegate to a separate file.

However, API later declares a delegate property of type APIDelegate and this
created a dependency on it. Draw an arrow from API pointing to APIDelegate to
show this.

API also declares a property for server, which is a String that it gets from the
configuration on the AppDelegate. Hence, it depends on both Configuration and
AppDelegate. Draw an arrow from API pointing at AppDelegate and another arrow
pointing at Configuration to show this.

Your dependency map should now look like this:

Oh no! You’ve found another circular dependency between AppDelegate and API.
Again, you’ll deal with this later.

raywenderlich.com 264
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

API also has a new dependency on Token. Draw a new box to the top right of API for
Token and draw an arrow from API pointing at it.

Lastly, API also has a dependency on URLSession. However, this is defined within
Foundation. As you did before with the system dependency on UIKit, you don’t
need to explicitly indicate this in the diagram.

The rest of this class doesn’t introduce any new dependencies, so you can move onto
the next file you need to inspect: UIViewController+Alert.swift. This file declares
an extension on UIViewController that has one new dependency on
ErrorViewController.

Draw a new box for ErrorViewController below UIViewController+Alert and draw


an arrow from the UIViewController+Alert box pointing at ErrorViewController
box.

Now, your dependency map should look like this:

raywenderlich.com 265
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Deciding when to stop


You could iteratively walk all files and create a diagram for the entire app. While this
might be interesting, it’d likely be too busy to be useful. The further you get from the
type you’re trying to modify, the less likely you’ll find relevant dependencies. Should
you find yourself making changes in files that aren’t on your diagram, of course, you
can always include them later.

As a sanity check to verify you’ve gone far enough, do a text search for
LoginViewController to see if any other files reference it. You’ll find
ErrorViewController actually has a reference to LoginViewController.

Open ErrorViewController and you’ll see that it uses LoginViewController in


secondaryAction(_:). Add another arrow pointing from ErrorViewController to
LoginViewController to represent this.

This reveals an indirect cycle between LoginViewController,


UIViewController+Alert and ErrorViewController.

ErrorViewController also has a property of type Skin. Add another arrow from
ErrorViewController pointing at Skin.

Ultimately, your diagram should look like this:

raywenderlich.com 266
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

What are problematic dependencies?


A type is coupled to another when it directly depends on it. However, this may or
may not be problematic. For example, if a type is coupled to a delegate protocol (e.g.
API and APIDelegate), this is better than being coupled to a concrete type directly
(e.g. LoginViewController).

Tight coupling refers to a “problematic” dependency that cannot be easily swapped


out. This begs the question: What is a problematic dependency? Simply put, a
dependency is problematic if it prevents you from accomplishing your goal.

In this case, your goal is to pull login into a separate module. Anything that prevents
this is a problematic dependency.

Practically speaking, how can you identify these problematic dependencies? Ask the
following of each direct dependency of LoginViewController:

1. Is the dependency on the AppDelegate? By definition, the AppDelegate


represents the app, so it cannot be pulled into the module. Hence, it’s going to be
problematic.

2. Is the dependency circular? If so, you may need to break one or both sides.

3. Does the dependency have many secondary dependencies? If so, it’s going to be
difficult to pull it into the module.

4. Does it make sense for the dependency to be pulled into the same module? Even
if it’s possible to pull the dependency into the same module, it may not be
appropriate to do so.

It may make sense to create another module but you should carefully plan what’s
best to do. This is especially true if the dependency is used in many places
throughout the app.

Finding problematic dependencies


You can evaluate the relationships in the dependency map using these questions to
find problematic dependencies.

First, are any dependencies on the AppDelegate? Yes, there are. The problematic
relationships are the arrows that point to to the AppDelegate. This includes
LoginViewController depending on the AppDelegate and API depending on the
AppDelegate.

raywenderlich.com 267
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Got your red marker handy? Highlight both of these arrows in red to indicate they’re
problematic.

You won’t ever be able to pull the AppDelegate into a module, so it’s problematic in
general. Highlight the AppDelegate box in red to indicate this.

Are there any circular dependencies? Yep, there are those too.
LoginViewController depends on API, which it gets from the AppDelegate. In turn,
AppDelegate depends on LoginViewController.

You’ve already identified the LoginViewController-to-AppDelegate relationship


as problematic. What about the AppDelegate-to-LoginViewController
relationship? Would this prevent you from pulling login into a separate module?

No, actually. AppDelegate could depend on the new login module and, in turn, it
could still set up the LoginViewController. Hence, it’s not problematic in terms of
your goal.

What about the LoginViewController-to-API relationship? Yes, this is a problem


for two reasons:

1. API is used in other places throughout the app, so it would be difficult to pull it
into the login module.

2. API doesn’t conceptually make sense in the login module. It knows about all of
the models and networking calls within the app. This is way beyond the scope
that login should know about.

Hence, highlight the LoginViewController-to-API arrow and the API box in red to
show these are problematic.

Does LoginViewController have any dependencies with many secondary


dependencies? Yes, APIDelegate depends on a lot of models.

Does the login module really need to know about any of these models? The two
APIDelegate methods that are related to login are loginFailed(error:) and
loginSucceeded(userId:). Neither of these actually use these models!

Consequently, the LoginViewController-to-APIDelegate relationship, the


LoginViewController-to-Models relationship and the APIDelegate box itself are
all problematic. Highlight each of these in red.

raywenderlich.com 268
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Your diagram should now look like this:

There are three remaining direct dependencies of LoginViewController: Skin,


Validators and UIViewController+Alert. Does it make sense to pull these into
the same module as login?

If Skin were only used by LoginViewController, it might be okay to pull it into the
same module. However, it’s also used by ErrorViewController, so it’s not okay to
do this. Highlight the LoginViewController-to-Skin relationship and the Skin box
itself in red.

raywenderlich.com 269
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Should Validators be moved into the same login module? Yes, actually! It’s only
used by LoginViewController and its methods are explicitly related to login
validation. Highlight this relationship and the Validators box in green to indicate
it’s okay to move.

Does it make sense for UIViewController+Alert to be in the same login module?


Nope, it’s a generic component and used in several places throughout the app. It may
actually make sense for this to be in a separate module itself but it doesn’t belong in
the login module. Therefore, highlight the LoginViewController-to-
UIViewController+Alert relationship and the UIViewController+Alert box itself
in red.

Ultimately, your dependency map should look like this:

raywenderlich.com 270
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Completing the map


If you find that a direct dependency is problematic, you don’t need to evaluate
whether its secondary dependencies are problematic. Rather, you’ll need to refactor
or fix this in some way first. Depending on what you do in this regard, however, you
may later consider the secondary dependencies or may never do this.

If you find that a dependency is okay, you do need to evaluate its secondary
dependencies. It could turn out some of the secondary dependencies are problematic
and you’ll need to add them somehow.

Once you’ve completed this for all relevant dependencies, you’re done with
evaluating the dependencies on your map!

In this case, you’ve actually completed both of these already, so your map is good as
is.

Breaking up complex systems


You can use your dependency map as a blueprint to break up complex systems. It
tells you exactly how types are related and which relationships are problematic!

Of course, there’s still the issue of actually addressing the problematic relationships.
You usually cannot simply delete a relationship, as it’s providing some sort of useful
functionality.

Practically speaking then, how can you fix these problems? Using TDD, of course!
Yes, there’s more to it than simply “magically TDD code it” but you’ll learn all about
this in the next chapter!

Key points
You learned about dependency maps in this chapter. Here are their key points:

• Dependency maps are a tool for visualizing your code dependencies.

• You can use them to discover problematic relationships.

• You can use them as a blueprint for breaking up a complex system.

raywenderlich.com 271
iOS Test-Driven Development by Tutorials Chapter 12: Dependency Maps

Where to go from here?


In the next chapter, you’ll use this dependency map to actually pull out the login
functionality into a new module! Of course, you’ll do this in a TDD fashion and learn
tricks along the way for handling problematic relationships.

Continue onto the next chapter to learn all about it!

raywenderlich.com 272
13 Chapter 13: Breaking Up
Dependencies
By Michael Katz

It’s always safer to make a change when you have tests in place already. In the
absence of existing tests, however, you may need to make changes just to add tests!
One of the most common reasons for this is tightly-coupled dependencies: You can’t
add tests to a class because it depends on other classes that depend on other
classes… View controllers especially are often victims of this issue.

By creating a dependency map in the last chapter, you were able to find where you
want to make changes and, in turn, where you really need to have tests.

This chapter will teach you how to break dependencies safely to add tests around
where you want to change.

raywenderlich.com 273
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Getting started
As a reminder, in this chapter, you will build upon and improve the MyBiz app. The
powers that be want to build a separate expense reporting app. In the interest of DRY
(Don’t Repeat Yourself) they want to reuse the login view from your app in the new
app. The best way to do that is to pull the login functionality into its own framework
so it can be reused across projects.

The login view controller is the obvious place to start because it presents the login
UI and uses all of the other code related to login. In the previous chapter, you built
out a dependency map for the login view controller and identified some change
points. You’ll use that map as a guide to break up the dependencies so login can
stand alone.

raywenderlich.com 274
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Characterizing the system


Before moving any code, you want to make sure that the refactors won’t disturb the
behavior of the app. To do that, start with a characterization test for the signIn(_:)
function of LoginViewController. This is the main entry point for signing into the
app and it’s crucial that it continues to work.

Add a new Unit Test Case Class file in CharacterizationTests ▸ Cases named
LoginViewControllerTests.swift.

Replace the contents of the file with the following:

import XCTest
@testable import MyBiz

class LoginViewControllerTests: XCTestCase {


var sut: LoginViewController!

// 1
override func setUpWithError() throws {
try super.setUpWithError()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "login")
as? LoginViewController
UIApplication.appDelegate.userId = nil

sut.loadViewIfNeeded()
}

// 2
override func tearDownWithError() throws {
sut = nil
UIApplication.appDelegate.userId = nil //do the "logout"
try super.tearDownWithError()
}

func testSignIn_WithGoodCredentials_doesLogin() {
// given
sut.emailField.text = "agent@shield.org"
sut.passwordField.text = "hailHydra"

// when
// 3
let predicate = NSPredicate { _, _ -> Bool in
UIApplication.appDelegate.userId != nil
}
let exp = expectation(
for: predicate,
evaluatedWith: sut,
handler: nil)

raywenderlich.com 275
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

sut.signIn(sut.signInButton!)

// then
// 4
wait(for: [exp], timeout: 5)
XCTAssertNotNil(
UIApplication.appDelegate.userId,
"a successful login sets valid user id")
}
}

This code handles the basic sign-in scenario in the following ways:

1. In setUpWithError() creates the sut from the main storyboard and loads it. It
also clears the shared userId from AppDelegate. It is a proxy for the “being
logged in” state. Since the app delegate is persisted across tests, it’s important to
clear it out so each test starts off as not logged in.

2. In tearDownWithError(), clearing that userId state is important in case there


are other tests that don’t clear it in their setUp.

3. In the test itself, this predicate expectation waits for the userId state to be set in
order to fulfill the expectation. This way, the test knows it is safe to proceed.

4. The test waits for the userId to be set and then asserts that it is not nil. Even
though the expectation will also time out for the same condition, it’s always good
to have an explicit assert — rather than using the timeout to catch the error.

Remember to start the backend before running this test — or it will fail! This test
requires live responses. You have not broken its dependency on the real backend
implementation yet. For instructions on setting up and starting the MyBiz backend,
see Chapter 11, “Legacy Problems”.

Build and test — and the test passes! This example short-circuits the discovery part
of characterization tests, as described in Chapter 11, “Legacy Problems”. It’s an
important part of the process but out of the scope of this chapter.

Next, capture the main error case in a test. This flow where an invalid login response
is shown to the user is an important function of the view controller. This also helps
cover detangling the ErrorViewController later.

Still in LoginViewControllerTests.swift, add the following test:

func testSignIn_WithBadCredentials_showsError() {
// given
sut.emailField.text = "bad@credentials.ca"
sut.passwordField.text = "Shazam!"

raywenderlich.com 276
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

// when
let predicate = NSPredicate { _, _ -> Bool in
UIApplication.appDelegate
.rootController?.presentedViewController != nil
}
let exp = expectation(
for: predicate,
evaluatedWith: sut,
handler: nil)

sut.signIn(sut.signInButton!)

// then
wait(for: [exp], timeout: 5)
let presentedController =
UIApplication.appDelegate
.rootController?
.presentedViewController
as? ErrorViewController
XCTAssertNotNil(
presentedController,
"should be showing an error controller")
XCTAssertEqual(
presentedController?.alertTitle,
"Login Failed")
XCTAssertEqual(
presentedController?.subtitle,
"Unauthorized")
}

• The given section sets up invalid credentials.

• The when section creates an expectation that waits for a modal view to be shown,
supposedly the error view.

• The then section, after waiting for the expectation, checks that the modal is an
ErrorViewController and that the alertTitle and subtitle match the
expected response for bad credentials.

These conditions are great because they test for a specific error, rather than a broken
network connection. However, this test is then quite brittle and dependent on server-
side text.

Build and test. Yet again this will pass and you’ve covered the two main (existing)
flows through this view controller. As a challenge, write tests for the validator
conditions as well (bad email and password).

raywenderlich.com 277
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Breaking up the API/AppDelegate


dependency
Now that there are some tests in place, it’s time to start breaking up the
dependencies so you can move the code. Starting with the API <-> AppDelegate
interdependency will make it easier to break up those classes from
LoginViewController later.

You can use Swift’s strict type system to make it easier when removing
dependencies. For example, go to API.swift and search the file for uses of
AppDelegate.

The first usage of AppDelegate creates the server constant. This one is quite simple
to deal with. You’ll just move it from being set automatically to an init parameter.

Replace the init method with:

init(server: String) {
self.server = server
session = URLSession(configuration: .default)
}

Next, update the line for let server = with the following:

let server: String

If you build the app, the compiler will tell you what needs to happen next. Go to
AppDelegate.swift and replace the instantiation of API with:

api = API(server: AppDelegate.configuration.server)

Going back to the tests, open MockAPI.swift, and add the following init method to
the MockAPI class:

init() {
super.init(server: "http://mockserver")
}

Build and test and the tests will still pass. This was a simple move so it doesn’t need
any additional testing beyond the tests already in place.

raywenderlich.com 278
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Using a notification for communication


The next step is to fix the logout dependency. This method calls back to app
delegate, but handling the post-logout state shouldn’t really live with an app
delegate. You’ll use a Notification to pass the event in a general way. You won’t fix
AppDelegate this time around, but you will make API ignorant of which class cares
about it.

At the top of API.swift, right after the import statement, add:

let userLoggedOutNotification =
Notification.Name("user logged out")

This creates a new notification that informs the rest of the app that the user logged
out.

Before proceeding, it’s time to create some tests! Create a New Unit Test Class
named APITests in the MyBizTests ▸ Cases target.

Replace the contents of the file with the following:

import XCTest
@testable import MyBiz

class APITests: XCTestCase {


var sut: API!

// 1
override func setUpWithError() throws {
try super.setUpWithError()
sut = MockAPI()
}

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}

// 2
func givenLoggedIn() {
sut.token = Token(token: "Nobody", userID: UUID())
}

// 3
func testAPI_whenLogout_generatesANotification() {
// given
givenLoggedIn()
let exp = expectation(
forNotification: userLoggedOutNotification,

raywenderlich.com 279
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

object: nil)

// when
sut.logout()

// then
wait(for: [exp], timeout: 1)
XCTAssertNil(sut.token)
}
}

1. This test sets up an API as the system-under-test. It’s okay that it’s a MockAPI
since the methods under test are inherited from API. This short-term
compromise is okay because its modified behavior is not part of the work of
breaking out LoginViewController.

2. There’s one helper method givenLoggedIn that sets a fake token to simulate the
“logged in” state for the SUT.

3. The test itself is pretty simple, calling logout and waiting for the
UserLoggedOutNotification. The test also asserts that the token was reset to
nil.

Run the tests. You’ll see that this new test does not yet pass. To get the test to pass,
open API.swift and replace the entire logout method with the following:

func logout() {
token = nil
delegate = nil
let note = Notification(name: userLoggedOutNotification)
NotificationCenter.default.post(note)
}

Instead of calling back directly into AppDelegate, this calls that code indirectly
through notification center. Now, API no longer has any direct dependency on the
delegate. However, to keep the app running, you need to make the following
changes.

raywenderlich.com 280
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Open AppDelegate.swift, and add the following method at the end of the class:

func setupListeners() {
NotificationCenter.default
.addObserver(
forName: userLoggedOutNotification,
object: nil,
queue: .main) { _ in
self.showLogin()
}
}

This adds a listener for the new notification. Then, in


application(_:didFinishLaunchingWithOptions:) call it before the return
statement by adding the following line of code:

setupListeners()

Build and test again. You can also build and run and then can go through a full login/
logout cycle to see that everything still works.

Reflecting on the breakup


This exercise illustrated two ways for detangling two objects:

1. Configuring the object at instantiation. API now has its server URL set at init
time rather than calling into a singleton later.

2. Replacing direct calls with events. Logout events are propagated through a
Notification instead of a hard-coded callback.

In logout, the call to AppDelegate was replaced by posting a Notification. As an


iOS developer, you have many options for sending asynchronous events.
NotificationCenter is the simplest since it comes with Foundation. You could also
send a signal using RxSwift or Combine, a custom event bus or manage a list of
custom delegates.

A further refactor to divide up responsibilities would be to extract user state


management from API. This would allow you to keep API as a stateless gateway to
the backend and the user state manager would be able to sit in between the UI and
the login/logout.

raywenderlich.com 281
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

The other technique used here was to pass in the configuration to the init method.
Here, all that was needed was the base URL for the server and there was no
functional reason to reach back to the AppDelegate. In fact, API no longer relies on
any UI code: You can go ahead and remove the import UIKit line from the top of
the file. Now, API can be used in all sorts of other apps that are built upon the same
API!

You can update the dependency map with a little white-out to reflect API’s
newfound freedom from the AppDelegate.

raywenderlich.com 282
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Breaking the AppDelegate dependency


The next stop on the dependency-detangling train is removing AppDelegate from
LoginViewController.

Injecting the API


In LoginViewController.swift, change the api variable to:

var api: API!

Now, the api can be set externally to the class instead of depending directly on
AppDelegate.

Note: For most classes, using let and injecting the value through an init is
the way to go. For view controllers, the injection will have to be done after
instantiation, usually in a prepare(for:sender:) with a segue or just before
presentation when done through code, as you’ll see below.

To make the app still work, you have to set the api variable in a few places. In
SceneDelegate.swift, add the following method
application(_:didFinishLaunchingWithOptions:):

func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {

let loginViewController =
UIApplication.appDelegate.rootController as?
LoginViewController
loginViewController?.api = UIApplication.appDelegate.api
}

This sets that api when the app, or more correctly, the window scene is first loaded.
At this point the login controller will be instantiated but not yet had its view loaded.

Next, change showLogin() by adding the following line immediately before setting
the rootViewController:

loginController?.api = api

raywenderlich.com 283
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Finally, since there was already a test to cover the view controller, you’ll need to
update the test class. In LoginViewControllerTests.swift, add to the bottom of
setUp(), just above sut.loadViewIfNeeded():

sut.api = UIApplication.appDelegate.api

If you build and either run or test, the app should continue to behave as before even
though you’ve broken one dependency.

Detangling login success


If you look at loginSucceeded(userId:) on the LoginViewController, you’ll see
that none of its contents really belong in the view controller — all of the work
happens on the AppDelegate! The issue then becomes how to indirectly link the API
action to a consequence in the AppDelegate. Well… last time you used a
Notification and you can do so again.

Add the following code to API.swift, just underneath the import statements:

let userLoggedInNotification =
Notification.Name("user logged in")

enum UserNotificationKey: String {


case userId
}

This adds a new notification for login and a key that will be used to get the user’s ID.

Before modifying more of the code, add the following test for the notification in
APITests.swift:

func testAPI_whenLogin_generatesANotification() {
// given
var userInfo: [AnyHashable: Any]?
let exp = expectation(
forNotification: userLoggedInNotification,
object: nil) { note in
userInfo = note.userInfo
return true
}

// when
sut.login(username: "test", password: "test")

// then
wait(for: [exp], timeout: 1)
let userId = userInfo?[UserNotificationKey.userId]

raywenderlich.com 284
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

XCTAssertNotNil(
userId,
"the login notification should also have a user id")
}

This test calls login(username:password:) and waits for the notification and
checks that the notification has a userId in its userInfo. Run your tests and you
will see that this test will not yet pass.

To get this test to pass, open API.swift and add the following to
handleToken(token:), just before the call to loginSucceeded(userId:):

let note = Notification(


name: userLoggedInNotification,
object: self,
userInfo: [
UserNotificationKey.userId: token.user.id.uuidString
])
NotificationCenter.default.post(note)

This code will post the Notification. To make sure it gets called in the test, add the
following override to MockAPI.swift:

override func login(username: String, password: String) {


let token = Token(token: username, userID: UUID())
handleToken(token: token)
}

Now, the test will build and pass! But you’re not done yet. You now need to move the
login functionality from LoginViewController to AppDelegate.

In AppDelegate.swift add the following helper function:

func handleLogin(userId: String) {


self.userId = userId

let storyboard = UIStoryboard(name: "Main", bundle: nil)


let tabController =
storyboard.instantiateViewController(
withIdentifier: "tabController")
rootController = tabController
}

raywenderlich.com 285
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

This does the same logic as the loginSucceeded(userId:) callback. Next, add the
following to setupListeners:

NotificationCenter.default
.addObserver(
forName: userLoggedInNotification,
object: nil,
queue: .main) { note in
if let userId =
note.userInfo?[UserNotificationKey.userId] as? String {
self.handleLogin(userId: userId)
}
}

This adds the listener for the notification. Finally, in LoginViewController.swift,


replace the contents of loginSucceeded(userId:) with an empty body.

If you build and test, the app will still have the same login/logout functionality —
even if the chain of events from a login is now a little different.

Now you can update the dependency map once again:

raywenderlich.com 286
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Breaking the ErrorViewController


dependency
Looking at the dependency map for red lines, it next makes sense to tackle the
dependency on LoginViewController from ErrorViewController.

It’s time to add characterization tests. Create a new Unit Test Case Class file named
ErrorViewControllerTests.swift in CharacterizationTests/Cases and replace its
contents with the following:

import XCTest
@testable import MyBiz

class ErrorViewControllerTests: XCTestCase {


var sut: ErrorViewController!

override func setUpWithError() throws {


try super.setUpWithError()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "error")
as? ErrorViewController
}

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}

func whenDefault() {
sut.type = .general
sut.loadViewIfNeeded()
}

func whenSetToLogin() {
sut.type = .login
sut.loadViewIfNeeded()
}

func testViewController_whenSetToLogin_primaryButtonIsOK() {
// when
whenSetToLogin()

// then
XCTAssertEqual(sut.okButton.currentTitle, "OK")
}

func testViewController_whenSetToLogin_showsTryAgainButton() {
// when
whenSetToLogin()

raywenderlich.com 287
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

// then
XCTAssertFalse(sut.secondaryButton.isHidden)
XCTAssertEqual(
sut.secondaryButton.currentTitle,
"Try Again")
}

func testViewController_whenDefault_secondaryButtonIsHidden()
{
// when
whenDefault()

// then
XCTAssertNil(sut.secondaryButton.superview)
}
}

This adds three simple tests for the state of each button in the error view controller:

• testViewController_whenSetToLogin_primaryButtonIsOK makes sure the


primary button is titled ‘OK’.

• testViewController_whenSetToLogin_showsTryAgainButton makes sure the


secondary button is titled ‘Try Again’.

• testViewController_whenDefault_secondaryButtonIsHidden makes sure that


there is no secondary button in the default, or general, case.

Run the tests and observe that they all pass.

Ideally, there should also be a test for the secondary button that actually results in a
try again action. Unfortunately, in its current state, it would be difficult to write a
unit test due to how intertwined this class is with LoginViewController. In fact,
that is one of the main motivators for breaking the dependency.

To write a test in the current state, you would have to script a good portion of the
app to get the ErrorViewController to be set up correctly and bring in a fair
amount of overall state to check that there was an effect when tapping the button.
So, leave it for now. You’ll capture the try again behavior as part of breaking up the
dependency.

raywenderlich.com 288
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Removing login from error handling


Now that you’ve got the base behavior covered, you’re ready to go ahead and start
breaking out the dependency. ErrorViewController has a “try again” function that
calls back into the LoginViewController. This not only violates SOLID principles but
it’s cumbersome to add this try again functionality to other screens since you’ll need
to add to several switch statements and further tie in dependencies.

The way to break out this dependency is with a form of the Command pattern. That is,
you’ll provide the necessary view information and behavior to the view controller so
the button can invoke the try again behavior at run time. This pattern is a way for
one object to provide implementation to another.

You’ll do this by adding the following struct at the top of the ErrorViewController
class above enum AlertType:

struct SecondaryAction {
let title: String
let action: () -> Void
}

This struct contains the view information — title — and the behavior — action
block. This is how view controllers will configure the error view going forward.

Create a new Unit Test Case Class in the MyBizTests target, named
ErrorViewControllerTests.swift. Then replace its contents with the following:

import XCTest
@testable import MyBiz

class ErrorViewControllerTests: XCTestCase {


var sut: ErrorViewController!

override func setUpWithError() throws {


try super.setUpWithError()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "error")
as? ErrorViewController
}

override func tearDownWithError() throws {


sut = nil
try super.tearDownWithError()
}

func testSecondaryButton_whenActionSet_hasCorrectTitle() {
// given
let action = ErrorViewController.SecondaryAction(

raywenderlich.com 289
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

title: "title") {}
sut.secondaryAction = action

// when
sut.loadViewIfNeeded()

// then
XCTAssertEqual(sut.secondaryButton.currentTitle, "title")
}

func testSecondaryAction_whenButtonTapped_isInvoked() {
// given
let exp = expectation(description: "secondary action")
var actionHappened = false
let action = ErrorViewController.SecondaryAction(
title: "action") {
actionHappened = true
exp.fulfill()
}
sut.secondaryAction = action
sut.loadViewIfNeeded()

// when
sut.secondaryAction(())

// then
wait(for: [exp], timeout: 1)
XCTAssertTrue(actionHappened)
}
}

This test follows the same pattern as your other view controller tests. There are two
test cases: testSecondaryButton_whenActionSet_hasCorrectTitle and
testSecondaryAction_whenButtonTapped_isInvoked. These cover only the new
functionality of using the SecondaryAction. The first tests that the button’s title is
set appropriately. The second checks that tapping the button performs the action
block.

Of course, this test won’t yet compile, let alone run. Now, head back to
ErrorViewController.swift.

Delete the AlertType enum. Then, replace the type variable with:

var secondaryAction: SecondaryAction?

This property allows you to store the optional action. Then add this helper method:

private func updateAction() {


guard let action = secondaryAction else {

raywenderlich.com 290
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

secondaryButton.removeFromSuperview()
return
}
secondaryButton.setTitle(action.title, for: .normal)
}

To use it, in viewDidLoad, replace the switch type {...} statement with:

updateAction()

Now, when the view is loaded, it will call the helper method to set up the button.

Also, remove the setupLogin() method. Then, replace the body of


secondaryAction(_:) with:

if let action = secondaryAction {


dismiss(animated: true)
action.action()
} else {
Logger.logFatal("no action defined.")
}

This replaces the call to the LoginViewController with a simple invocation of the
action block.

Now, if you try to build the project, you’ll see a compiler error. To begin fixing it,
navigate to UIViewController+Alert.swift. Update the
showAlert(title:subtitle:type:skin:) function signature with:

func showAlert(
title: String,
subtitle: String?,
action: ErrorViewController.SecondaryAction? = nil,
skin: Skin? = nil
) {

This updates the alert to take an action instead of a type.

Next, replace:

alertController.type = type

with the following:

alertController.secondaryAction = action

raywenderlich.com 291
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Next, in LoginViewController.swift replace loginFailed(error:) with:

func loginFailed(error: Error) {


let retryAction = ErrorViewController.SecondaryAction(
title: "Try Again") { [weak self] in
if let self = self {
self.signIn(self)
}
}
showAlert(
title: "Login Failed",
subtitle: error.localizedDescription,
action: retryAction,
skin: .loginAlert)
}

This updated method uses the new showAlert signature to use the new action
instead of type.

Finally, to finish the refactor, navigate to ErrorViewControllerTests.swift and make


the following changes:

First, in whenDefault(), remove the sut.type = .general line.

Then, in whenSetToLogin, replace the sut.type = .login line with

sut.secondaryAction = .init(title: "Try Again") {}

Build and test and your tests will compile and pass. This means
ErrorViewController is free from LoginViewController and you’re ready to move
on to create a separate login module!

raywenderlich.com 292
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Take a look at your updated dependency map. There is a lot less red now:

raywenderlich.com 293
iOS Test-Driven Development by Tutorials Chapter 13: Breaking Up Dependencies

Challenge
This chapter’s challenge is a simple one. You may have noticed that input validation
was left out of the LoginViewControllerTests characterization tests. Your
challenge is to add them now, so you will have a more robust test suite before
moving the code into its own module in the next chapter. For an additional
challenge, add unit tests for the Validators functions in MyBizTests.

Key Points
• Dependency Maps are your guide to breaking dependencies.

• Break up bad dependencies one at a time, using techniques like dependency


inversion, command patterns, notifications and configuring objects from the
outside.

• Write tests before, during and after a large refactor.

Where to go from here?


Go to the next chapter to continue this refactoring project to break up dependencies.
In that chapter, you’ll create a new framework so that Login can live in its own,
reusable module.

It’s also worth revisiting Section 3 on networking. The techniques taught in this
section will help explain how to fix LoginViewControllerTests so that you could
break up API and test its methods without having to use the MockAPI class.

raywenderlich.com 294
14 Chapter 14: Modularizing
Dependencies
By Michael Katz

Splitting an app into modules, whether they be frameworks, static libraries or just
structurally-isolated code, is an important part of clean coding. Having files with
related concerns at the same level of abstraction makes your code easier to maintain
and reuse across projects.

In this chapter, you’ll continue the work from the last chapter, further breaking
MyBiz into modules so you can reuse the login functionality. You’ll learn how to
define clean boundaries in the code to create logical units. Through the use of tests,
you’ll make sure the new architecture works and the app continues to function.

raywenderlich.com 295
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Making a place for the code to go


There are several ways to modularize an app. In this tutorial, you’ll use the most
common and easiest: A new dynamic framework. You can reuse a framework in many
iOS projects and distribute it through tools like Cocoapods, Carthage or Swift
Package Manager.

Even if you completed the challenge from the last chapter, start with this chapter’s
starter project. That way, you won’t have any discrepancies with file or test names.

Let’s start by creating the new framework:

1. From the Project editor, create a new target. Choose the Framework template to
create a dynamic framework and click Next.

2. Set the Product Name to Login.

3. Make sure you’ve checked Include Unit Tests. This sets you up to add tests right
away!

4. Click Finish.

5. Select the newly-created LoginTests target and change Host Application to


None, if it isn’t already.

6. Select Build Phases and make sure Login is the only dependency. Remove
MyBiz as a dependency.

raywenderlich.com 296
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Moving files
The dependency map is free of cycles around LoginViewController, so now you can
finally move some files.

Grab the two “green” files LoginViewController.swift and Validators.swift and


drag them from the MyBiz target to the Login target.

Double-check that you’ve changed these files’ Target Membership from MyBiz to
Login.

Build and run your app and you’ll see a lot of red errors. LoginViewController may
now be free of bad dependencies, but it’s not free of dependencies altogether. You’ll
see by the number of issues that this won’t be as easy as it might first seem.

raywenderlich.com 297
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

First, classes like Skin and ErrorViewController are dependencies of both


LoginViewController and other classes in MyBiz. To prevent copying or
introducing circular dependencies, you’ll need to create yet another framework.

Create a new Framework named UIHelpers using the same steps as above. Be sure
to also Include Unit Tests.

Move the following files to the new target:

• UIViewController+Alert.swift

• ErrorViewController.swift

• Skin.swift

• Styler.swift

• Colors.swift

To simplify this refactoring process, switch the scheme to the auto-created one for
UIHelpers. This way, only this library will build, which will reduce the noise from
other build errors.

Breaking up Styler’s dependencies


The first error you may notice is in Styler.swift. Styler relies on a configuration
from the AppDelegate. It breaks encapsulation to refer to the app delegate in this
helper framework, so you’ll need another way to set the configuration.

Configuration itself is also an issue because, in addition to UI styling, it contains


things like business logic and server setup. The easiest way to move forward is to
start from the bottom and move your way up.

Create a new Swift File in the UIHelpers target: UIConfiguration.swift.

Move the UI substruct from Configuration.swift to this new file and rename it
UIConfiguration:

struct UIConfiguration: Codable {


struct Button: Codable {
let cornerRadius: Double
let borderWidth: Double
}
let button: Button
}

raywenderlich.com 298
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Next, in Styler.swift change the let configuration line to:

var configuration: UIConfiguration?

This has to be set before you can use it; it’s no longer guaranteed to be set.

In style(button:skin:), replace:

button.layer.cornerRadius
= CGFloat(configuration.ui.button.cornerRadius)
button.layer.borderWidth
= CGFloat(configuration.ui.button.borderWidth)

with the following:

button.layer.cornerRadius
= CGFloat(configuration?.button.cornerRadius ?? 0)
button.layer.borderWidth
= CGFloat(configuration?.button.borderWidth ?? 0)

Finally, in ErrorViewController.swift, you’ll see a dependency on Logger. You’ll


revisit this dependency in this chapter’s challenge section. For now, comment out
this line of code.

Now, the framework will build successfully.

Note: It would be reasonable to perform the same dependency map exercise


on these files as you did for LoginViewController. That would involve going
through ErrorViewController’s dependencies to find the problematic
relationships and correct them. We bypassed that step here because it’s
straightforward and also so this tutorial could fit into a book.

raywenderlich.com 299
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Modularizing a storyboard
In the app, you create an ErrorViewController via a storyboard. You do this
explicitly in UIViewController+Alert.swift through the Main storyboard. Since this
storyboard lives in an app module, it’s not available to this framework.

To fix this, move the view controller to a new storyboard in the UIHelpers
framework by following these steps:

1. Open Main.storyboard and select the Error View Controller Scene.

2. Now, you can use an Xcode tool to help. Select Editor ▸ Refactor to
Storyboard….

3. Name it UIHelpers.storyboard.

4. Change the Group to UIHelpers.

5. Uncheck the MyBiz target and check the UIHelpers target instead.

6. Click Save.

7. In Main.storyboard, delete the Error Scene reference.

Next, in UIViewController+Alert.swift, replace the let alertController = ...


with the following:

let thisBundle = Bundle(for: ErrorViewController.self)


let storyboard = UIStoryboard(
name: "UIHelpers",
bundle: thisBundle)
let alertController
= storyboard.instantiateViewController(
withIdentifier: "error")
as! ErrorViewController

This now loads the same scene, but from a new storyboard that lives within the
framework.

raywenderlich.com 300
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Moving tests
What about the tests? You already have some test cases that cover
ErrorViewController. You can move those, too.

In UIHelpersTests delete UIHelpersTests.swift.

Next, create a Cases group in UIHelpersTests and move


ErrorViewControllerTests.swift from MyBizTests to it. Verify that the target
membership changed to UIHelpersTests. Change the @testable import line to:

@testable import UIHelpers

Next, replace setUpWithError with the following:

override func setUpWithError() throws {


try super.setUpWithError()
sut = UIStoryboard(
name: "UIHelpers",
bundle: Bundle(for: ErrorViewController.self))
.instantiateViewController(withIdentifier: "error")
as? ErrorViewController
}

This new setUpWithError uses the new UIHelpers.storyboard you created.

Make sure that you’ve enabled the tests in this target. To check, open the Test
Navigator, right-click on UIHelpersTests and select Enable “UIHelperTests”.

Now, you can build and test just the UIHelpers scheme and feel a bit more confident
that this major refactor will work.

Note: For some of the characterization tests, you need a live connection to the
backend. The setup and launch instructions are in Chapter 11, “Legacy
Problems”.

raywenderlich.com 301
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Using the new framework with Login


Now that you have given the UI helpers their own framework, you need to tell the
Login framework about it.

In the Project editor, select Login. Under Frameworks and Libraries, add
UIHelpers. When that’s done, it should look like this:

Add this import to the top of LoginViewController.swift:

import UIHelpers

Next, you’ll have to fix some access levels in UIHelpers. When all the files were in
the same target, the default internal access was fine, but now you’ll need to make
some things public.

In UIHelpers make the following things public:

• Skin.

• All of the static let constants in Skin.

• Styler.

• In Styler: class, shared, configuration and all the style methods.

• In UIViewController+Alert.swift: showAlert.

• UIConfiguration.

• All of the class var in Colors.swift.

• ErrorViewController and its SecondaryAction and viewDidLoad().

raywenderlich.com 302
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

You also need to add this initializer to SecondaryAction:

public init(title: String, action: @escaping () -> Void) {


self.title = title
self.action = action
}

This now exposes these types and functions for other modules to consume. In this
case, those modules will be Login and MyBiz.

Further isolating LoginViewController


Change the build scheme now to Login and build and run. You’ll still get a lot of
compiler errors.

Cleaning up LoginViewController will require fixing a long-time annoyance: The


API class is too broad and relies on weird delegates with lots of extra methods. You
can scope API by creating a new protocol that only contains the pieces related to
Login.

Create a new Swift file named LoginAPI under Login and replace its contents with
the following:

public protocol LoginAPI {


func login(
username: String,
password: String,
completion: @escaping (Result<String, Error>) -> Void
)
}

This code accomplishes two life-changing code and architectural clean-ups. First,
LoginAPI only has the one method that concerns Login. Second, it replaces the
obnoxious catch-all delegate with a simple completion block that uses a Result.
Conceptually, it would also make sense to add Logout, but you can save that for a
future improvement.

To make use of the new protocol, go back to LoginViewController:

1. Change the type of api to LoginAPI!.

2. In viewDidLoad(), remove the line api.delegate = self.

raywenderlich.com 303
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

3. Replace signIn(_:) with:

@IBAction func signIn(_ sender: Any) {


guard let username = emailField.text,
let password = passwordField.text else { return }
guard username.isEmail && password.isValidPassword else {
// a little client-side validation ;)
showAlert(
title: "Invalid Username or Password",
subtitle: "Check the username or password")
return
}
api.login(username: username, password: password) { result in
if case .failure(let error) = result {
self.loginFailed(error: error)
}
}
}

4. In the extension, remove the APIDelegate type, and remove every method other
than loginFailed(error:).

Ah, so much cleaner! I can’t understate the power of this change. To see the results
visually, look at this updated dependency map, now API is no longer in the picture:

raywenderlich.com 304
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Don’t forget the tests


Next, you’ll want to add tests to your protocol to verify the changes you’ve just
made.

First, grab ValidatorsTests.swift and drag it to the LoginTests target, making sure
the target changes to LoginTests as well.

Next, delete LoginTests.swift since you don’t need this file.

Finally, open ValidatorsTests.swift, and replace:

@testable import MyBiz

with the following:

@testable import Login

Build and run the Login target and tests to verify everything is working as intended.
The same conditions apply as with UIHelpersTests: Make sure there is no host
application and the target and scheme do not try to build MyBiz.

Fixing MyBiz
Now that you have two new frameworks that contain previously-available code,
you’ll need to fix up the dependencies in their usage project. Switch back to the
MyBiz scheme and you’ll start seeing all sorts of build errors.

Don’t worry, you’ll, tackle them one at a time and the project will straighten out in a
jiffy (or is it giffy? :]).

First, add the following import statement to the following files:

import UIHelpers

• DateSelectingViewController.swift

• AnnouncementsTableViewController.swift

• CreatePurachaseOrderTableViewController.swift

• PurchasesTableViewController.swift

• OrgTableViewController.swift

raywenderlich.com 305
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

• AddToOrderTableViewController.swift

• Configuration.swift

Next, in Configuration.swift, replace:

let ui: UI

with the following:

let ui: UIConfiguration

This takes care of the UIHelpers framework, but you’ll also need to use the Login
framework.

Open AppDelegate.swift, and add the following below import UIKit:

import Login

To fix the errors, open LoginViewController.swift and make


LoginViewController, viewDidLoad() and api public.

Also add the import Login to SceneDelegate.swift as well.

Now it’s time to tackle the trickiest part: The API.

Open API.swift, and add the following below import Foundation:

import Login

Next, you’ll want to replace the existing login. Still inside API.swift, replace the
existing login(username:password:) and handleToken(token:) with the
following:

func login(
username: String,
password: String,
completion: @escaping (Result<String, Error>) -> Void
) {
let eventsEndpoint = server + "api/users/login"
let eventsURL = URL(string: eventsEndpoint)!
var urlRequest = URLRequest(url: eventsURL)
urlRequest.httpMethod = "POST"
let data = "\(username):\(password)".data(using: .utf8)!
let basic = "Basic \(data.base64EncodedString())"
urlRequest.addValue(
basic,
forHTTPHeaderField: "Authorization")

raywenderlich.com 306
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

let task = session.dataTask(with: urlRequest) {


data, _, error in
guard let data = data else {
if error != nil {
DispatchQueue.main.async {
completion(.failure(error!))
}
}
return
}
let decoder = JSONDecoder()
if let token = try? decoder.decode(Token.self, from: data) {
self.handleToken(token: token, completion: completion)
} else {
do {
let error = try decoder.decode(
APIError.self,
from: data)
DispatchQueue.main.async {
completion(.failure(error))
}
} catch {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
}

task.resume()
}

func handleToken(
token: Token,
completion: @escaping (Result<String, Error>) -> Void
) {
self.token = token
Logger.logDebug("user \(token.user.id)")
DispatchQueue.main.async {
let note = Notification(
name: userLoggedInNotification,
object: self,
userInfo: [
UserNotificationKey.userId: token.user.id.uuidString
])
NotificationCenter.default.post(note)
completion(.success(token.user.id.uuidString))
}
}

raywenderlich.com 307
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

This code is mostly the same as before except instead of calling the delegate, it now
calls the passed in completion block instead. Finally, change the class definition to:

class API: LoginAPI

Next, you can clean up APIDelegate by removing loginFailed(error:) and


loginSucceeded(userId:) from the protocol definition.

Finally, remove loginFailed(error:) and loginSucceeded(userId:) from the


APIDelegate conformance extensions in the following files:

• AnnouncementsTableViewController.swift

• CalendarModel.swift

• OrgTableViewController.swift

• PurchasesTableViewController.swift

• CreatePurachaseOrderTableViewController.swift

• SettingsTableViewController.swift

Build and run MyBiz and you’ll find it now builds. If only that was all you needed to
do…

Fixing the storyboard


Even though the app builds, it does not yet run or pass the tests. The next stop on
this refactor train is to work on the storyboard.

Open Main.storyboard. Select the Login View Controller Scene. In the Identity
inspector, change the Module to Login. You could also extract a separate storyboard
for the login framework, which is part of this chapter’s challenge, which you’ll come
to later.

Now the app will build and run and work just the same as before.

raywenderlich.com 308
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Fixing the tests


There are a few build issues to fix in the tests.

In MockAPI.swift, replace the existing override of login with:

override func login(


username: String,
password: String,
completion: @escaping (Result<String, Error>) -> Void
) {
let token = Token(token: username, userID: UUID())
handleToken(token: token, completion: completion)
}

This code adds the completion argument introduced when you replaced the login
and handleToken methods above.

In SpyAPI.swift, add the completion argument you created earlier by replacing the
override of login with the following:

override func login(


username: String,
password: String,
completion: @escaping (Result<String, Error>) -> Void
) {
loginCalled = true
super.login(
username: username,
password: password,
completion: completion)
}

Next, open APITests.swift, find testAPI_whenLogin_generatesANotification(),


and replace the when line with the following:

sut.login(username: "test", password: "test") { _ in }

Open LoginViewControllerTests.swift, and add the following below @testable


import MyBiz:

@testable import Login


@testable import UIHelpers

raywenderlich.com 309
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Next, in the characterization test ErrorViewControllerTests.swift, add the


following below @testable import MyBiz:

@testable import UIHelpers

Finally, to use the correct storyboard, replace setUpWithError with the following:

override func setUpWithError() throws {


try super.setUpWithError()
sut = UIStoryboard(
name: "UIHelpers",
bundle: Bundle(for: ErrorViewController.self))
.instantiateViewController(withIdentifier: "error")
as? ErrorViewController
}

Now, all tests will pass once again, and you can take a deep sigh of relief. The
refactor didn’t break anything!

Wrap up
Pat yourself on the back. Login is now in its own framework and ready to be re-used
in another project. You’ll have to distribute both the Login framework and the
UIHelpers frameworks, but it’s normal for frameworks to have their own
dependencies.

Take a look at the final dependency map, updated to reflect the changes to API:

It’s a nice, clean and hierarchical diagram. There are no cycles and you haven’t
pulled in any extraneous data types or unrelated functionality. Good job!

raywenderlich.com 310
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Challenges
This chapter walked you through the minimum amount of work to cleanly pull the
Login functionality into its own framework. However, there’s (a lot of!) room for
improvement. Fix up the project by completing any of the following:

1. LoginViewController still relies on Main.storyboard in the MyBiz module,


which makes it harder to reuse. Pull it out into its own storyboard that lives
within the framework.

2. Add and improve the login tests by:

• Pulling the LoginViewControllerTests characterization tests into the


LoginTests target.

• Repurposing those test cases as unit tests by creating a mock LoginAPI so you
don’t have to go through API and the local server.

• Creating an AppDelegateTests that tests the user state flow.

3. Fix the Logger issue by either bringing it into UIHelpers and passing its
configuration in like Styler OR by creating a logging protocol and attaching it to
the frameworks.

Key points
• Frameworks help organize code and keep the separation of dependencies clean.

• Use protocols to provide implementation from callers without creating circular


dependencies.

• Write tests before, during and after a large refactor.

raywenderlich.com 311
iOS Test-Driven Development by Tutorials Chapter 14: Modularizing Dependencies

Where to go from here?


Gosh, that was a lot of work, but you really cleaned up the code. There are a few areas
that are worth investigating in the future to improve your architectural hygiene.
Some of these were suggested in the Challenge, but you can achieve even more
improvement with a dedicated user state manager, and by using a pattern like Router
or FlowController to handle showing the error and login screens, rather than relying
upon AppDelegate.

Other great resources are the original Design Patterns book (Gamma et al) which,
although very object-oriented, contains a lot of useful patterns for incrementally
separating dependencies and breaking out functionality. More immediately useful
would be these architecture books at https://www.raywenderlich.com/books:

• Design Patterns by Tutorials

• Combine: Asynchronous Programming With Swift or RxSwift: Reactive Programming


With Swift

• Advanced iOS App Architecture

raywenderlich.com 312
15 Chapter 15: Adding
Features to Existing Classes
By Michael Katz

You won’t always have the time, or it may simply not be feasible, to break
dependencies of a very large class. If you have a deadline to add a new feature and
your app is a bowl of spaghetti, you won’t have the time to straighten it all out first.
Fortunately, there are TDD techniques to tackle this situation.

In this chapter, you’ll learn strategies to add functionality to an existing class, while
at the same time, avoiding modifying it! To do this, you’ll learn strategies like
sprouts and dependency injection.

To demonstrate these ideas, you’ll add some basic analytics to the MyBiz app’s main
view controllers. After all, every business wants to know what their users are doing.

Getting started
Use the back-end and starter projects from this chapter, as they have a few
modifications from the last chapter that you’re going to need. Start up the back end.
As always, refer back to Chapter 11, “Legacy Problems” if you need help getting it
running.

Your objective is to add a screen to view analytics events for each of the five main
view controllers: Announcements, Calendar, Org Chart, Purchase Orders and
Settings. This way, the product owners will be able to identify the most-used screens,
to figure out where to invest time and resources.

raywenderlich.com 313
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Reporting an analytics event involves:

• A user-initiated action, like a screen view or button tap.

• A Report that contains the metadata for the event.

• Sending that report to the back end.

Sending reports
It will be easiest, in this case, to start from the bottom up: Adding the ability to send
reports to the back end. You already have a class that communicates with the back
end, API. You’ll create an extension for this class to handle the new functionality
while avoiding bloating the current file.

Laying a foundation
First things first, take what you learned in the previous chapter and start with a
protocol to keep the dependencies clean and make the work easier to test.

Create a new group named Analytics in the starter project under the MyBiz group.
You’ll use this to organize all the analytics-related code and will make the project
easier to navigate. It should have been better organized from the beginning, but you
don’t always get to choose your starting project. Move Report.swift to this group.
This file holds Report, which represents an individual analytics event to send back
to the server.

Next, in that group, create a new Swift file named AnalyticsAPI.swift. You’ll use
this to define a protocol to keep the analytics work separate from other back-end
functions.

Replace the contents of AnalyticsAPI.swift with the following placeholder code:

protocol AnalyticsAPI {

raywenderlich.com 314
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Whenever you add new code, you should add tests first. In the MyBizTests/Cases
group, create a new Unit Test Case Class named AnalyticsAPITests and add it to
the MyBizTests target.

Replace the contents of the file with the following:

import XCTest
@testable import MyBiz

class AnalyticsAPITests: XCTestCase {


var sut: AnalyticsAPI!

override func setUpWithError() throws {


try super.setUpWithError()
}

override func tearDownWithError() throws {


try super.tearDownWithError()
}

func testAPI_whenReportSent_thenReportIsSent() {
// given
let date = Date()
let interval: TimeInterval = 20.0
let report = Report(
name: "name",
recordedDate: date,
type: "type",
duration: interval,
device: "device",
os: "os",
appVersion: "appVersion")

// when send a report?


// ???

// then assert a report was sent


// ???
}
}

testAPI_whenReportSent_thenReportIsSent() assumes AnalyticsAPI can send


a Report, and then you’ll be able to verify that it was sent. The only question is how?
There’s no good extension point in the app to easily do this.

raywenderlich.com 315
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Extending the API


The first step is to send the report. You already have a class that sends stuff to the
back end: API. As you may have seen from previous chapters, this class is
cumbersome and is interwoven with the rest of the app code. Ideally, you want to add
new functionality to it without increasing its complexity or introducing new
dependencies.

Thankfully, Swift allows you to split implementation across files through the use of
extensions. Using extensions, you can add new functionality to API for analytics
without having to perturb the existing mess any more than necessary.

Create a new file in the Analytics group: API+Analytics.swift. This naming


convention lets you know that the file will contain an extension of API that has
something to do with analytics.

Next, add the following extension to the file:

extension API: AnalyticsAPI {


}

You now have a concrete AnalyticsAPI you can use in your test.

Go back to AnalyticsAPITests.swift and replace sut, setUpWithError and


tearDownWithError with the following:

var sut: AnalyticsAPI { return sutImplementation }


var sutImplementation: API!

override func setUpWithError() throws {


try super.setUpWithError()
sutImplementation = API(server: "test")
}

override func tearDownWithError() throws {


sutImplementation = nil
try super.tearDownWithError()
}

This creates a specific API instance stored in sutImplementation, but exposes it


only as an AnalyticsAPI through the variable sut. This way, you can be sure you’re
testing only AnalyticsAPI’s methods and not any other logic that might come along
with API.

raywenderlich.com 316
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Sending a report
Now you can start thinking about that report.

Open AnalyticsAPI.swift add the following method to the protocol:

func sendReport(report: Report)

Next, open API+Analytics.swift and add the following implementation to the


extension:

func sendReport(report: Report) {


}

Now, you have a method to send the report you can use within the test.

Open AnalyticsAPITests.swift, find testAPI_whenReportSent_thenReportIsSent


and replace the when section with the following:

// when
sut.sendReport(report: report)

The hard part is figuring out how to test that the report was sent. This is a unit test,
so you don’t want to rely on a live back end to verify the app logic. On of top that,
the test instance of API doesn’t even have a valid URL to call!

What you really want is a mock object that stands in for the back end, but also uses
the real API implementation. If you just mock AnalyticsAPI, then the test would
only verify that, when you call an object method, the method executes. So you need
the real API.

To get around this, another protocol and injection comes to the rescue! Open
API.swift and add the following protocol to the file:

protocol RequestSender {
func send<T: Decodable>(
request: URLRequest,
success: ((T) -> Void)?,
failure: ((Error) -> Void)?
)
}

This method takes a URLRequest, sends it and reports back successes or failures in
one or the other completion blocks.

API already has a method that basically does this, so it will be easy to implement.

raywenderlich.com 317
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Add the following extension to the bottom of the file:

extension API: RequestSender {


func send<T>(
request: URLRequest,
success: ((T) -> Void)?,
failure: ((Error) -> Void)?
) where T: Decodable {
let task = loadTask(
request: request,
success: success,
failure: failure)
task.resume()
}
}

This reuses loadTask(request:success:failure:) to make a URLSessionTask


and to forward the success and failure blocks. The method also starts the task,
since it doesn’t return a value and there’s no other way to execute task.

Finally, add the following var to API below var token:

lazy var sender: RequestSender = self

This sets up a request sender that can be injected later, but uses itself as a default.
This will be a leverage point for adding testing to API in the next step. It may seem a
little indirect to have a self-reference like this. However, taking this step allows you
to otherwise leave this class untouched and still add new functionality to it,
including testing.

Testing the API


In the MyBizTests target, create a new group: Mocks. In that group, create a new
file, MockSender.swift, and replace its contents with the following:

import XCTest
@testable import MyBiz

class MockSender: RequestSender {


var lastSent: Decodable?

func send<T: Decodable>(


request: URLRequest,
success: ((T) -> Void)?,
failure: ((Error) -> Void)?
) {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

raywenderlich.com 318
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

do {
let obj = try decoder.decode(
T.self,
from: request.httpBody!)
lastSent = obj
success?(obj)
} catch {
print("error decoding a \(T.self): \(error)")
failure?(error)
}
}
}

This class implements the RequestSender protocol by returning the object you used
to create the request body then storing it in lastSent. There are a lot of things you
could do from here, but this is sufficient to finish the test.

Go back to AnalyticsAPITests.swift and add a variable for the mock:

var mockSender: MockSender!

Next, add the following to the end of setUpWithError :

mockSender = MockSender()
sutImplementation.sender = mockSender

Next, add the following to tearDownWithError, just before the call to super:

mockSender = nil

Finally replace the then section of testAPI_whenReportSent_thenReportIsSent


with the following:

// then
XCTAssertNotNil(mockSender.lastSent)
XCTAssertEqual(report.name, "name")
XCTAssertEqual((mockSender.lastSent as? Report)?.name, "name")

Remember that MockSender stores the sent object in lastSent, so you’re able to use
this to verify the passed-in Report was sent. Build and run the test and you’ll see it
still fails. You still need to supply the implementation for sendReport(report:).

raywenderlich.com 319
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Sprouting the send method


API already has a method that takes an object and sends it to the back end:
submitPO(po:). It’s too bad that this is specifically for sending purchase orders. You
could refactor this method by mapping its dependencies, writing characterization
and unit tests, and expanding the API functionality in a reusable way.

BUT, you don’t have time for that amount of refactoring right now. In this case,
you’re going to do something your teachers told you never to do: Copy code. It’s
okay. You’re going to have tests for this copied method, and this work is only meant
to support you as you add analytics. You will go back and finish the refactor after you
get this working.

Open API+Analytics.swift and add the following API extension to the end of the
file:

extension API {
// 1
func logAnalytics(
analytics: Report,
completion: @escaping (Result<Report, Error>) -> Void
) throws {
// 2
let url = URL(string: server + "api/analytics")!
var request = URLRequest(url: url)
if let token = token?.token {
let bearer = "Bearer \(token)"
request.addValue(
bearer,
forHTTPHeaderField: "Authorization")
}
request.addValue(
"application/json",
forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"

let coder = JSONEncoder()


coder.dateEncodingStrategy = .iso8601
let data = try coder.encode(analytics)
request.httpBody = data

// 3
sender.send(
request: request,
success: { savedEvent in
completion(.success(savedEvent))
},
failure: { error in
completion(.failure(error))

raywenderlich.com 320
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

})
}
}

This code replicates the code of submitPO(po:) with a few notable changes:

1. logAnalytics(analytics:completion:) takes an analytics Report instead of a


PurchaseOrder. Also, importantly, it has a completion block which returns a
Result instead of relying on the hard-to-understand, and probably buggy,
delegate that came with the original app code. Taking advantage of new
language features and modern patterns is a good idea if you can roll them out as
you improve or add code.

2. Instead of the hard-coded endpoint for purchase orders, this has a hard-coded
analytics endpoint.

3. This uses the new RequestSender.send(request:success:failure:) that you


introduced to API earlier. This means that you’ll be able to test this method!

Here, you’re using a technique called sprouting a method, which is when you add a
new method in an existing class that enhances or duplicates existing functionality so
you can add a new feature. This technique allows you to sidestep going down a hole
refactoring a class or potentially breaking things not yet under test. It allows you to
define a new interface, cleanly separated from the legacy part of the code. In this
case, the interface is even defined in a separate file.

To finish up this task, connect logAnalytics to AnalyticsAPI by adding the


following to sendReport(report:) in the extension:

try? logAnalytics(analytics: report) { _ in }

You call logAnalytics(analytics:completion:), passing the report and a blank


completion and no error handling.

Now, build and test and the tests will pass. You’ve successfully added a new (and
testable!) method to API with only minimal intrusion into the existing codebase.

raywenderlich.com 321
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Adding analytics to the view controllers


The hard work is over, and the rest should be easy, right? If you think back to the list
of steps for analytics, you still need to implement this part:

• A user-initiated action, like a screen view or button tap.

You’ll start with the leftmost view controller:


AnnouncementsTableViewController. First, create a new Unit Test Case Class in
MyBizTests/Cases named AnnouncementsTableViewControllerTests.swift.

Finally, replace the contents of the file with the following:

import XCTest
@testable import MyBiz

class AnnouncementsTableViewControllerTests: XCTestCase {

var sut: AnnouncementsTableViewController!

override func setUp() {


super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier:
"announcements")
as? AnnouncementsTableViewController
}

override func tearDown() {


sut = nil
super.tearDown()
}

func whenShown() {
sut.viewWillAppear(false)
}

func testController_whenShown_sendsAnalytics() {
// when
whenShown()

// then the report will be sent


// ???
}
}

raywenderlich.com 322
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

This sets up a test where the system under test is an


AnnouncementsTableViewController. The purpose of
testController_whenShown_sendsAnalytics is to test that viewWillAppear(_:)
will result in an analytics report being sent. whenShown triggers this step. The next
step is figuring out how to verify that.

Not mocking all of the API


You’ve already set up a protocol to help out with the testing: AnalyticsAPI. You
don’t need to use API or mock out the RequestSender at all.

In the Mocks group, create a new Swift File named MockAnalyticsAPI.swift and
replace its contents with the following:

import XCTest
@testable import MyBiz

class MockAnalyticsAPI: AnalyticsAPI {


var reportSent = false

func sendReport(report: Report) {


reportSent = true
}
}

This class implements AnalyticsAPI, but instead of sending the report on, it uses
reportSent to flag that it triggered. Your previous tests on API ensure that the
report will make its way to the server.

Back in AnnouncementsTableViewControllerTests.swift, add a new var to the


class:

var mockAnalytics: MockAnalyticsAPI!

Next, add the following to the end of setUpWithError:

mockAnalytics = MockAnalyticsAPI()
sut.analytics = mockAnalytics

This creates the new mock and sets it on the sut.

raywenderlich.com 323
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Next, add the following to tearDownWithError:

mockAnalytics = nil

Next, in testController_whenShown_sendsAnalytics add the following to the


then condition:

XCTAssertTrue(mockAnalytics.reportSent)

Recall that MockAnalyticsAPI sets reportSent to false on initialization, and a


successful sendReport(report:) should set it to true. This allows the test to verify
the report will be sent.

Finally, to get the test to build and pass, you need to wire up viewWillAppear(_:)
to the analytics API. In AnnouncementsTableViewController.swift add the
following below var announcements:

var analytics: AnalyticsAPI?

Finally, add the following to the end of viewWillAppear(_:):

let screenReport = Report(


name: AnalyticsEvent.announcementsShown.rawValue,
recordedDate: Date(),
type: AnalyticsType.screenView.rawValue,
duration: nil,
device: UIDevice.current.model,
os: UIDevice.current.systemVersion,
appVersion: Bundle.main.object(
forInfoDictionaryKey: "CFBundleShortVersionString")
as! String)
analytics?.sendReport(report: screenReport)

This creates a Report with some useful information about the app, device and the
specific event. You then hand it off to AnalyticsAPI, which sends it to the back end.

Build and test; you’re back to green.

Another interesting use case


To implement the prior test, you set up a whole mock instance of AnalyticsAPI. You
can use this for testing, without having to worry about the messiness that was
previously built into MockAPI as a subclass of API. By using this protocol and starting
with a mock implementation, you’ll ensure by default that any new methods you add
to the app will be testable.

raywenderlich.com 324
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Another thing you can do with mocks is to verify the number of times a method is
called or the order in which methods are called.

Open MockAnalyticsAPI.swift, and add the following below var reportSent:

var reportCount = 0

Next, add the following to the end of sendReport(report:):

reportCount += 1

Now, every time you call sendReport(report:), reportCount increments.

Next, add the following test at the end of


AnnouncementsTableViewControllerTests.swift:

func testController_whenShownTwice_sendsTwoReports() {
// when
whenShown()
whenShown()

// then
XCTAssertEqual(mockAnalytics.reportCount, 2)
}

This tests that each time the screen displays, it will send a report. Build and test and
you should be all green.

Passing around dependencies


The analytics feature now works in tests, but not when you run the app. That’s
because you still need to pass AnalyticsAPI to the
AnnouncementsTableViewController.

When using storyboards, you want to do this in a prepare(for:sender:) segue


method to inject whatever dependencies you need into the next view controller (or,
similarly, in a view model or other helper). This app uses a plain
UITabBarController that’s manually added to the screen: There’s no
prepare(for:sender:) method to override.

Therefore, you have to set analytics manually, too. You know that you’re
potentially going to add it to many view controllers. It makes sense to think about a
way that you can add it to existing classes with minimal impact. That means
protocols to the rescue, once again.

raywenderlich.com 325
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Open AnalyticsAPI.swift and add the following protocol to the end of the file:

protocol ReportSending: AnyObject {


var analytics: AnalyticsAPI? { get set }
}

By adding a var and adhering to this protocol in any class, you’ll be able to inject an
AnalyticsAPI implementation.

Open AnnouncementsTableViewController.swift and add the following extension


to the end of the file:

extension AnnouncementsTableViewController: ReportSending {}

And, just like that, you can provide AnnouncementsTableViewController an


analytics object without exposing any additional information about itself.

Open AppDelegate.swift, replace handleLogin(userId:) with the following:

func handleLogin(userId: String) {


self.userId = userId

let storyboard = UIStoryboard(name: "Main", bundle: nil)


let tabController = storyboard
.instantiateViewController(withIdentifier: "tabController")
as! UITabBarController

tabController.viewControllers?
.compactMap { $0 as? ReportSending }
.forEach { $0.analytics = api }

rootController = tabController
}

This now adds an AnalyticsAPI to all of the tab bar’s view controllers that adhere to
ReportSending. Because that includes AnnouncementsTableViewController, you’ll
now see logging whenever its viewWillAppear(_:) fires.

raywenderlich.com 326
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Build and run the app. After logging in, the AnnouncementsTableViewController
tab will display. Open http://localhost:8080/api/analytics in a browser and
you’ll see recorded events similar to those below:

Adding more events


So you now have one screen sending reports. It should be straightforward to add
reports to additional screens. For example, in OrgTableViewController.swift add
the following var:

var analytics: AnalyticsAPI?

Finally, add the following extension to the end of the file:

extension OrgTableViewController: ReportSending {}

To implement ReportSending on this controller, start with a test. Create a new Unit
Test Case Class named OrgTableViewControllerTests.swift. Open
MyBizTests\Cases and replace the contents with the following:

import XCTest
@testable import MyBiz

class OrgTableViewControllerTests: XCTestCase {

var sut: OrgTableViewController!


var mockAnalytics: MockAnalyticsAPI!

override func setUp() {


super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "org")
as? OrgTableViewController

raywenderlich.com 327
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

mockAnalytics = MockAnalyticsAPI()
sut.analytics = mockAnalytics
}

override func tearDown() {


sut = nil
mockAnalytics = nil
super.tearDown()
}

func whenShown() {
sut.viewWillAppear(false)
}

func testController_whenShown_sendsAnalytics() {
// when
whenShown()

// then
XCTAssertTrue(mockAnalytics.reportSent)
}
}

This should look familiar, as it’s very similar to


AnnouncementsTableViewControllerTests.
testController_whenShown_sendsAnalytics tests that a report is sent when
OrgTableViewController displays.

To get the test to pass, OrgTableViewController will need to send the report when
its view displays. But, before modifying viewWillAppear(_:), it would be a good
idea to create a helper method so you don’t have to copy over the boilerplate.

Open Report.swift and add the following method to Report:

static func make(event: AnalyticsEvent,


type: AnalyticsType) -> Report {
Report(
name: event.rawValue,
recordedDate: Date(),
type: type.rawValue,
duration: nil,
device: UIDevice.current.model,
os: UIDevice.current.systemVersion,
appVersion: Bundle.main.object(
forInfoDictionaryKey: "CFBundleShortVersionString")
as! String)
}

raywenderlich.com 328
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

This factory method takes care of all the constants that go into a report, so the caller
only has to worry about the specifics on each screen. You should be comfortable
enough with TDD at this point to write a test for it on your own (Check out
ReportTests.swift in the final project if you want a hint).

You can now use this method in OrgTableViewController.swift. Add the following
to the end of viewWillAppear(_:):

let report = Report.make(


event: .orgChartShown,
type: .screenView)
analytics?.sendReport(report: report)

Now, the tests will pass. Build and run, and you should see two different screen
events recorded as you change tabs.

Congrats, you’ve managed to add a new feature to a reasonably-complicated app.


You’ve done so with minimal changes to the existing code and you’ve written tests
along the way.

raywenderlich.com 329
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Challenge
There are few tasks left undone that you should take care of:

• Clean up the AnnouncementsTableViewController to use the Report.make


method.

• Add screenView analytics to the other screens. As a hint, you’ll have to forward
the AnalyticsAPI through UINavigationControllers.

Key points
• You don’t have to bring a whole class under test to add new functionality.

• You can sprout a method to extend functionality, even if it adds a little


redundancy.

• Use protocols to inject dependencies and extensions to separate new code from
legacy code.

• TDD methods will guide the way for clean and tested features.

raywenderlich.com 330
iOS Test-Driven Development by TutorialsChapter 15: Adding Features to Existing Classes

Where to go from here?


Although you’ve come a long way, you’ve just scratched the surface of making
changes and improving code. You can continue to decompose API into specific
protocols like AnalyticsAPI and LoginAPI. You can also now incrementally improve
API by replacing delegates with Results and using the RequestSender to make the
code more testable.

You can also rework RequestSender into its own object to pass into API that
contains the server details. Then you could replace MockAPI in the existing tests so
you can write better and more comprehensive unit tests. This eliminates the need for
characterization tests to contact a live sever altogether. Your work is never done.

This approach also has some downsides. The indirection introduced by lots of small
protocols can make the code harder to debug, which is why having comprehensive
tests is crucial. When sprouting methods, it can be tempting to never to go back and
revisit your old code, leaving the app in a state that might be confusing for
newcomers. It also means the legacy code never improves.

This is the end of the legacy code tutorials. Check out Design Patterns by Tutorials
https://www.raywenderlich.com/books/design-patterns-by-tutorials for more
techniques for reorganizing and isolating code.

raywenderlich.com 331
16 Conclusion

What a journey it’s been! From exploring the TDD Cycle to testing networking code
and finally introducing tests to legacy applications which were not written to be
testable, you’ve gotten a taste for testing iOS apps using the latest techniques.

By putting the book concepts to practice, you’ll be confident your codebase will be
able to withstand changes even to critical path code since any regressions should be
caught with your written tests.

If you have any questions or comments as you work through this book, please stop by
our forums at https://forums.raywenderlich.com and look for the particular forum
category for this book.

Thank you again for purchasing this book. Your continued support is what makes the
books, tutorials, videos and other things we do at raywenderlich.com possible. We
truly appreciate it!

Happy testing!

– The iOS Test-Driven Development by Tutorials team

raywenderlich.com 332

You might also like