SlideShare a Scribd company logo
Testing the waters of
iOS
Kostas Kremizas
Twitter: @kostaskremizas
Testing the waters of iOS
Testing the waters of iOS
Testing the waters of iOS
The ONE TRUE WAY™ to write code
Testing the waters of iOS
Testing the waters of iOS
Testing the waters of iOS
Testing the waters of iOS
Look no more!
There are no silver
bullets!
!
!
!
!
ONE TRUE WAY™
Let's talk about Testing
Different types of tests
1. Manual tests
2. UI tests (XCUITest)
3. Integration tests
4. Unit tests (XCTest)
Testing the waters of iOS
What should I test?
Testing the waters of iOS
Seriously now, what should I test?
• Code that is very critical - unless it has been tested
otherwise
• Code that is complex (high cyclomatic complexity)
• Code that is likely to stick around but change often
• Code that I want to make cleaner
• Code that has turned out to be buggy
• New code
What are unit tests?
Code that tests if a specific unit of code works as
expected
A unit is the smallest testable piece of
code in the program.
• Object oriented -> Classes
• Functional -> functions
The anatomy of a unit
• The System Under Test (SUT)
• It's collaborators
class ColaWallet {
private var euros: Int
private let colaProvider: ColaProvider
init(euros: Int, colaProvider: ColaProvider) {
self.euros = euros
self.colaProvider = colaProvider
}
func buy(numberOfBottles: Int) -> [ColaBottle] {
euros -= 2
return colaProvider.get(numberOfBottles: numberOfBottles)
}
}
The anatomy of a unit test
• Arrange
• Act
• Assert
Arrange
• Create collaborators that you need to pass to the SUT upon
creation
• Create the sut and inject these collaborators
class ColaWalletTests: XCTestCase {
func testBuyMethod() {
// Arrange
let colaProvider = ColaProvider(colaBottles: 50)
let sut = ColaWallet(euros: 20, colaProvider: colaProvider)
// Act
let bottlesJustBought = sut.buy(numberOfBottles: 2)
// Assert
XCTAssertEqual(sut.euros, 16)
XCTAssertEqual(bottlesJustBought.count, 2)
XCTAssertEqual(colaProvider.colaBottles, 48)
}
}
Act
• Perform the action we are testing
class ColaWalletTests: XCTestCase {
func testBuyMethod() {
// Arrange
let colaProvider = ColaProvider(colaBottles: 50)
let sut = ColaWallet(euros: 20, colaProvider: colaProvider)
// Act
let bottlesJustBought = sut.buy(numberOfBottles: 2)
// Assert
XCTAssertEqual(sut.euros, 16)
XCTAssertEqual(bottlesJustBought.count, 2)
XCTAssertEqual(colaProvider.colaBottles, 48)
}
}
Assert
Check if the action performed as expected by checking
• the return value (if exists)
• how the sut's or the collaborator's state was affected
class ColaWalletTests: XCTestCase {
func testBuyMethod() {
// Arrange
let colaProvider = ColaProvider(colaBottles: 50)
let sut = ColaWallet(euros: 20, colaProvider: colaProvider)
// Act
let bottlesJustBought = sut.buy(numberOfBottles: 2)
// Assert
XCTAssertEqual(sut.euros, 16)
XCTAssertEqual(bottlesJustBought.count, 2)
XCTAssertEqual(colaProvider.colaBottles, 48)
}
}
Good unit tests should be
1. [F]ast
2. [I]solated
3. [R]epeatable
4. [S]elf-validating
5. [T]imely
How can we make tests fast,
isolated and repeatable?
• Avoid shared state between tests
• Avoid slow actions (DB Access, accessing network)
• Avoid actions you cannot control their result
The usual suspects:
• URLSession.shared
• UserDefaults.standard
• Date()
• UIApplication.shared
• FileManager.default
...
• Your own singletons
How to deal with them
1. Locate collaborators that are singletons or use singletons
indirectly via their own dependencies
2. Extract them as properties
3. Make them injectable
4. Create a protocol that wraps the signatures of their methods
that SUT uses
5. In the tests, inject a test double that conforms to this protocol
Example: User Defaults
Before
class SettingsViewController {
func saveTheme(isDark: Bool) {
UserDefaults.standard.set(isDark, forKey: darkTheme)
}
func isThemeDark() -> Bool {
return UserDefaults.standard.bool(forKey: darkTheme)
}
}
class SettingsViewControllerTests: XCTestCase {
var sut: SettingsViewController!
override func setUp() {
super.setUp()
UserDefaults.standard.set(nil, forKey: darkTheme)
sut = SettingsViewController()
}
func testSavingTheme() {
sut.saveTheme(isDark: true)
XCTAssertTrue(UserDefaults.standard.bool(forKey: darkTheme))
}
}
Extract injectable property
class SettingsViewController {
let defaults: UserDefaults
public init(store: UserDefaults) {
self.defaults = store
}
func saveTheme(isDark: Bool) {
defaults.set(isDark, forKey: darkTheme)
}
func isThemeDark() -> Bool {
return defaults.bool(forKey: darkTheme)
}
}
Use a protocol instead of the real UserDefaults class
protocol KeyValueStore {
func bool(forKey defaultName: String) -> Bool
func set(_ value: Bool, forKey defaultName: String)
}
extension UserDefaults: KeyValueStore {}
class SettingsViewController {
let store: KeyValueStore
public init(_ store: KeyValueStore) {
self.store = store
}
func saveTheme(isDark: Bool) {
store.set(isDark, forKey: darkTheme)
}
func isThemeDark() -> Bool {
return store.bool(forKey: darkTheme)
}
}
In the tests inject the SUT with an InMemoryStore that
conforms to the protocol
class InMemoryStore: KeyValueStore {
var booleanKeyValues = [String: Bool]()
func bool(forKey defaultName: String) -> Bool {
return booleanKeyValues[defaultName] ?? false
}
func set(_ value: Bool, forKey defaultName: String) {
booleanKeyValues[defaultName] = value
}
}
class SettingsViewControllerTests: XCTestCase {
var sut: SettingsViewController!
var store: InMemoryStore!
override func setUp() {
super.setUp()
store = InMemoryStore()
sut = SettingsViewController(store)
}
func testSavingTheme() {
sut.saveTheme(isDark: true)
XCTAssertTrue(store.bool(forKey: darkTheme))
}
}
Types of dependency injection
• Initialiser injection (already saw this)
• Property injection
• Parameter injection
Property injection
func testFoo() {
let sut = SUT()
let collab = Collab()
sut.collab = collab
let expectedResult = sut.foo()
XCTassertTrue(expectedResult)
}
Parameter injection
func testFoo() {
let sut = SUT()
let collab = Collab()
let expectedResult = sut.foo(collab)
XCTassertTrue(expectedResult)
}
Test doubles
Dummy
An object we just want to pass as an argument but don't care
about how it's used. Usually it returns nil or does nothing in it's
methods' implementations
class DummyPersonManager: PersonManager {
func findPerson(withName name: String) -> String? { return nil }
func store(_ person: Person) {}
}
Fake
Same behaviour with the collaborator but lighter
implementation without unwanted sidefects
Example: InMemoryStore we saw earlier
Stub
An object whose method's return values we control because the
SUT uses it and we want to be able to predict the result of a unit test.
struct AlwaysValidValidatorStub: FormDataValidator {
func isValid(_ data: FormData) -> Bool { return true }
}
// configurable across different tests
struct ValidatorStub: FormDataValidator {
var isValid: Bool
func isValid(_ data: FormData) -> Bool { return isValid }
}
Spy
!
A Stub that can also track which of it's methods where called, with what
parameters, how many times, etc.
struct AlwaysValidValidatorSpy: FormDataValidator {
var isValidCallParameters: [FormData] = []
var isValidCalled { return !isValidCallParameters.isEmpty }
func isValid(_ data: FormData) -> Bool {
isValidCallParameters.append(data)
return true
}
}
Mock
A Spy that can also verify that a specific set of preconditions have happened
struct ChrisLattnerValidatorMock: FormDataValidator {
var isValidCallParameters: [FormData] = []
var isValidCalled { return !isValidCallParameters.isEmpty }
func isValid(_ data: FormData) -> Bool {
isValidCallParameters.append(data)
return true
}
func validate() -> Bool {
return isValidCallParameters.map { $0.name }.contains("Chris Lattner")
}
}
Test Driven Developent (TDD)
Software development process relying on unit tests and on the
RED -> GREEN -> REFACTOR cycle
Testing the waters of iOS
The 3 laws of TDD
• You are not allowed to write any production code unless it is
to make a failing unit test pass.
• You are not allowed to write any more of a unit test than is
sufficient to fail; and compilation failures are failures.
• You are not allowed to write any more production code than
is sufficient to pass the one failing unit test.
Example: Story point calculator
Make it work,
make it right,
make it fast.
— Kent Beck
Testing the waters of iOS
TDD benefits
1. Breaks complex problems into small simple problems you can focus
on -> confidence when writing new code
2. Tests serve as up to date documentation
3. writing test first makes production code testable / losely coupled
4. Reduced debugging (localized test failures in the code modified
since last success)
5. Refactor more frequently and without fear (in every cycle)
6. Forces us to raise unit coverage
Unit testing tips and
tricks
Use Xcode keyboard shortcuts for testing
• ⌘ U Runs all tests
• ctrl ⌥ ⌘ U Runs the current test method
• ctrl ⌥ ⌘ G Re-runs the last run
• ctrl ⌘ U Run all tests without building
Enable sounds for success / failure
- Execute in parallel
- Randomize execution order
Gather coverage
To mock or not to
mock?
Mock a collaborator if
• it is slow (e.g. network, db, ui)
• it's methods have undeterministic results (e.g. network, db,
current time)
• it's uninspectable (analytics)
• it's methods heve deterministic but difficult to predict
results (high cyclomatic complexity)
Testing SUT without mocking
• many codepaths
• complex Arrange
• code losely coupled with tests
Mock collabs | test collabs separately
• minimum codepaths
• easier arrange step
• tests tightly coupled with code
• more fragile tests
struct SUT {
let collab1: Collab
// would need 2*50 = 100 tests to test everything through the SUT without mocking collaborator
// needs 2 tests for testing this function if collaborator is mocked + 50 tests for the collaborator
func doSth(baz: Bool) -> Int {
if baz {
return collab.calculate() // 1
} else {
return collab.calculate() // 2
}
}
}
struct Collab {
let a: A
// would need 50 tests
func calculate() -> Int {
switch a.value {
case 1:
return 99
case 2:
return 100
case 3:
return 12
// .
// .
default: // case 50
return 50
}
}
}
Mock only direct
collaborators
Testing the waters of iOS
Dependency graph
Mock indirect collaborator
!
Mock direct collaborator
!
Testing the waters of iOS
Use factories
for creating complex collaborators if you don't mock them
The problem
Deep dependency graph + Swift type safety = Arrange step
hell
!
Testing the waters of iOS
func testCanNotifyIfPersonHasValidEmailOrIsNearby() {
let sut = Notifier(location: .athensOffice)
let address = Address(city: "",
country: "",
street: "",
number: "",
postCode: "",
floor: 4,
coords: CLLocationCoordinate2D(latitude: 37.988067, longitude: 23.734227))
let kostas = Person(id: 0,
firstName: "",
lastName: "",
address: address,
email: "krem@mailcom",
phone: 0)
XCTAssertTrue(sut.canNotify(kostas))
}
Testing the waters of iOS
Testing the waters of iOS
Seems legit!
func testCanNotifyIfPersonHasValidEmailOrIsNearby() {
let kostas = Person.fromJSONFile(withName: "kostas")
XCTAssertTrue(sut.canNotify(kostas))
}
But!
• No way to tell from the unit test which parts of the person object are being used by
the tested mehtod
• JSON == no type safety -> Each time the collaborator type changes you have to
manually edit countless JSON files
We want to go from
func testCanNotifyIfPersonHasValidEmailOrIsNearby() {
let sut = Notifier(location: .athensOffice)
let address = Address(city: "",
country: "",
street: "",
number: "",
postCode: "",
floor: 4,
coords: CLLocationCoordinate2D(latitude: 37.98, longitude: 23.73))
let kostas = Person(id: 0,
firstName: "",
lastName: "",
address: address,
email: "krem@mailcom",
phone: 0)
XCTAssertTrue(sut.canNotify(kostas))
}
To
func testCanNotifyIfPersonHasValidEmailOrIsNearby() {
let sut = Notifier(location: .athensOffice)
let kostas = Person.make(
address: .make(coords: CLLocationCoordinate2D(latitude: 37.98, longitude: 23.73)),
email: "")
XCTAssertTrue(sut.canNotify(kostas))
}
What we need is initializers or static factory methods with
default values for all parameters so that we can provide only
the ones we need for our test.
Code generation to
the rescue!
Using Sourcery templates
Generated code
// in your test target
extension Person {
static func make(id: Int = Int.defaultValue,
firstName: String = String.defaultValue,
lastName: String = String.defaultValue,
address: Address = Address.make(),
email: String = String.defaultValue,
phone: Int = Int.defaultValue) -> Person {
return self.init(id: id,
firstName: firstName,
lastName: lastName,
address: address,
email: email,
phone: phone)
}
}
extension Address {
static func make(city: String = String.defaultValue,
country: String = String.defaultValue,
street: String = String.defaultValue,
number: String = String.defaultValue,
postCode: String = String.defaultValue,
floor: Int = Int.defaultValue,
coords: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0)) -> Address {
return self.init(city: city,
country: country,
street: street,
number: number,
postCode: postCode,
floor: floor,
coords: coords)
}
}
TDD + Playgrounds =
• How to do it
• Faster feedback
!
• Playgrounds are still unstable
☹
• Diffucult to use dependencies
• Which pushes you to think twice about design
• You can also use TestDrive to easily use pods
• No full inline failure explanation (only in logs)
Behavior-Driven Development
• Uses a DSL and human language sentences
• Closer to user story specs
• Given When Then structure
• Nesting contexts -> keeps tests dry
Quick + Nimble
Take small steps!
• Remember the 3 rules of TDD.
• Stay GREEN as much as possible
• Refactoring == changing code - not behaviour while
GREEN
Testing the waters of iOS
legacy code is simply
code without tests
— Michael Feathers
Working Effectively with Legacy Code
Testing the waters of iOS
Testing the waters of iOS
Testing the waters of iOS
Resources on Testing / TDD
• StoryPointCalculator Demo project
• AutoMake sourcery template
• Quality coding blog
• GeePaw Blog
• Refactoring: Improving the Design of Existing Code
• Working Effectively with Legacy Code
• Swift by Sundell blog- Testing category
• BDD with Quick and Nimble

More Related Content

DOC
Sample notice of ruling for California
PDF
Unit Test and TDD
PDF
Swift testing ftw
PDF
Taking the boilerplate out of your tests with Sourcery
PDF
Unit testing in xcode 8 with swift
PDF
Unit Testing & TDD Training for Mobile Apps
PDF
Unit Tesing in iOS
PDF
TDD - Test Driven Development in Swift (iOS) with Cuckoo, Quick and Nimble
Sample notice of ruling for California
Unit Test and TDD
Swift testing ftw
Taking the boilerplate out of your tests with Sourcery
Unit testing in xcode 8 with swift
Unit Testing & TDD Training for Mobile Apps
Unit Tesing in iOS
TDD - Test Driven Development in Swift (iOS) with Cuckoo, Quick and Nimble

Similar to Testing the waters of iOS (20)

PDF
A Comprehensive Guide to Efficiently Writing and Implementing iOS Unit Tests.pdf
PDF
Automated Testing on iOS
PPT
Nguyenvandungb seminar
PDF
TDD by Controlling Dependencies
PPTX
Getting Started with XCTest and XCUITest for iOS App Testing
PPTX
Unit Testing and Why it Matters
PPTX
L2624 labriola
PPTX
Test Driven Development on Android (Kotlin Kenya)
PDF
The Cowardly Test-o-Phobe's Guide To Testing
PDF
[XCode] Automating UI Testing
PDF
XCTest_ A Complete Comprehensive Guide.pdf
PPTX
In search of JavaScript code quality: unit testing
PPTX
JS Frameworks Day April,26 of 2014
PDF
Js fwdays unit tesing javascript(by Anna Khabibullina)
PDF
Golang dot-testing-lite
PDF
Test driven development
PDF
Intro To JavaScript Unit Testing - Ran Mizrahi
PDF
Unit test documentation
PDF
Property-Based Testing in Objective-C & Swift with Fox
PDF
Testing, Learning and Professionalism — 20171214
A Comprehensive Guide to Efficiently Writing and Implementing iOS Unit Tests.pdf
Automated Testing on iOS
Nguyenvandungb seminar
TDD by Controlling Dependencies
Getting Started with XCTest and XCUITest for iOS App Testing
Unit Testing and Why it Matters
L2624 labriola
Test Driven Development on Android (Kotlin Kenya)
The Cowardly Test-o-Phobe's Guide To Testing
[XCode] Automating UI Testing
XCTest_ A Complete Comprehensive Guide.pdf
In search of JavaScript code quality: unit testing
JS Frameworks Day April,26 of 2014
Js fwdays unit tesing javascript(by Anna Khabibullina)
Golang dot-testing-lite
Test driven development
Intro To JavaScript Unit Testing - Ran Mizrahi
Unit test documentation
Property-Based Testing in Objective-C & Swift with Fox
Testing, Learning and Professionalism — 20171214
Ad

Recently uploaded (20)

PDF
Approach and Philosophy of On baking technology
PDF
Diabetes mellitus diagnosis method based random forest with bat algorithm
PDF
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
PDF
cuic standard and advanced reporting.pdf
PDF
Machine learning based COVID-19 study performance prediction
PPTX
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
PDF
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
PDF
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
PPTX
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
PDF
Peak of Data & AI Encore- AI for Metadata and Smarter Workflows
PDF
Spectral efficient network and resource selection model in 5G networks
PPTX
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
PPTX
Understanding_Digital_Forensics_Presentation.pptx
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PDF
Encapsulation theory and applications.pdf
PDF
Advanced methodologies resolving dimensionality complications for autism neur...
PDF
Empathic Computing: Creating Shared Understanding
PDF
KodekX | Application Modernization Development
PDF
Encapsulation_ Review paper, used for researhc scholars
PDF
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
Approach and Philosophy of On baking technology
Diabetes mellitus diagnosis method based random forest with bat algorithm
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
cuic standard and advanced reporting.pdf
Machine learning based COVID-19 study performance prediction
KOM of Painting work and Equipment Insulation REV00 update 25-dec.pptx
Build a system with the filesystem maintained by OSTree @ COSCUP 2025
Architecting across the Boundaries of two Complex Domains - Healthcare & Tech...
VMware vSphere Foundation How to Sell Presentation-Ver1.4-2-14-2024.pptx
Peak of Data & AI Encore- AI for Metadata and Smarter Workflows
Spectral efficient network and resource selection model in 5G networks
ACSFv1EN-58255 AWS Academy Cloud Security Foundations.pptx
Understanding_Digital_Forensics_Presentation.pptx
The Rise and Fall of 3GPP – Time for a Sabbatical?
Encapsulation theory and applications.pdf
Advanced methodologies resolving dimensionality complications for autism neur...
Empathic Computing: Creating Shared Understanding
KodekX | Application Modernization Development
Encapsulation_ Review paper, used for researhc scholars
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
Ad

Testing the waters of iOS

  • 1. Testing the waters of iOS Kostas Kremizas Twitter: @kostaskremizas
  • 5. The ONE TRUE WAY™ to write code
  • 11. There are no silver bullets!
  • 13. Let's talk about Testing
  • 14. Different types of tests 1. Manual tests 2. UI tests (XCUITest) 3. Integration tests 4. Unit tests (XCTest)
  • 16. What should I test?
  • 18. Seriously now, what should I test? • Code that is very critical - unless it has been tested otherwise • Code that is complex (high cyclomatic complexity) • Code that is likely to stick around but change often • Code that I want to make cleaner • Code that has turned out to be buggy • New code
  • 19. What are unit tests? Code that tests if a specific unit of code works as expected
  • 20. A unit is the smallest testable piece of code in the program. • Object oriented -> Classes • Functional -> functions
  • 21. The anatomy of a unit • The System Under Test (SUT) • It's collaborators class ColaWallet { private var euros: Int private let colaProvider: ColaProvider init(euros: Int, colaProvider: ColaProvider) { self.euros = euros self.colaProvider = colaProvider } func buy(numberOfBottles: Int) -> [ColaBottle] { euros -= 2 return colaProvider.get(numberOfBottles: numberOfBottles) } }
  • 22. The anatomy of a unit test • Arrange • Act • Assert
  • 23. Arrange • Create collaborators that you need to pass to the SUT upon creation • Create the sut and inject these collaborators
  • 24. class ColaWalletTests: XCTestCase { func testBuyMethod() { // Arrange let colaProvider = ColaProvider(colaBottles: 50) let sut = ColaWallet(euros: 20, colaProvider: colaProvider) // Act let bottlesJustBought = sut.buy(numberOfBottles: 2) // Assert XCTAssertEqual(sut.euros, 16) XCTAssertEqual(bottlesJustBought.count, 2) XCTAssertEqual(colaProvider.colaBottles, 48) } }
  • 25. Act • Perform the action we are testing class ColaWalletTests: XCTestCase { func testBuyMethod() { // Arrange let colaProvider = ColaProvider(colaBottles: 50) let sut = ColaWallet(euros: 20, colaProvider: colaProvider) // Act let bottlesJustBought = sut.buy(numberOfBottles: 2) // Assert XCTAssertEqual(sut.euros, 16) XCTAssertEqual(bottlesJustBought.count, 2) XCTAssertEqual(colaProvider.colaBottles, 48) } }
  • 26. Assert Check if the action performed as expected by checking • the return value (if exists) • how the sut's or the collaborator's state was affected
  • 27. class ColaWalletTests: XCTestCase { func testBuyMethod() { // Arrange let colaProvider = ColaProvider(colaBottles: 50) let sut = ColaWallet(euros: 20, colaProvider: colaProvider) // Act let bottlesJustBought = sut.buy(numberOfBottles: 2) // Assert XCTAssertEqual(sut.euros, 16) XCTAssertEqual(bottlesJustBought.count, 2) XCTAssertEqual(colaProvider.colaBottles, 48) } }
  • 28. Good unit tests should be 1. [F]ast 2. [I]solated 3. [R]epeatable 4. [S]elf-validating 5. [T]imely
  • 29. How can we make tests fast, isolated and repeatable? • Avoid shared state between tests • Avoid slow actions (DB Access, accessing network) • Avoid actions you cannot control their result
  • 30. The usual suspects: • URLSession.shared • UserDefaults.standard • Date() • UIApplication.shared • FileManager.default ... • Your own singletons
  • 31. How to deal with them 1. Locate collaborators that are singletons or use singletons indirectly via their own dependencies 2. Extract them as properties 3. Make them injectable 4. Create a protocol that wraps the signatures of their methods that SUT uses 5. In the tests, inject a test double that conforms to this protocol
  • 32. Example: User Defaults Before class SettingsViewController { func saveTheme(isDark: Bool) { UserDefaults.standard.set(isDark, forKey: darkTheme) } func isThemeDark() -> Bool { return UserDefaults.standard.bool(forKey: darkTheme) } } class SettingsViewControllerTests: XCTestCase { var sut: SettingsViewController! override func setUp() { super.setUp() UserDefaults.standard.set(nil, forKey: darkTheme) sut = SettingsViewController() } func testSavingTheme() { sut.saveTheme(isDark: true) XCTAssertTrue(UserDefaults.standard.bool(forKey: darkTheme)) } }
  • 33. Extract injectable property class SettingsViewController { let defaults: UserDefaults public init(store: UserDefaults) { self.defaults = store } func saveTheme(isDark: Bool) { defaults.set(isDark, forKey: darkTheme) } func isThemeDark() -> Bool { return defaults.bool(forKey: darkTheme) } }
  • 34. Use a protocol instead of the real UserDefaults class protocol KeyValueStore { func bool(forKey defaultName: String) -> Bool func set(_ value: Bool, forKey defaultName: String) } extension UserDefaults: KeyValueStore {} class SettingsViewController { let store: KeyValueStore public init(_ store: KeyValueStore) { self.store = store } func saveTheme(isDark: Bool) { store.set(isDark, forKey: darkTheme) } func isThemeDark() -> Bool { return store.bool(forKey: darkTheme) } }
  • 35. In the tests inject the SUT with an InMemoryStore that conforms to the protocol class InMemoryStore: KeyValueStore { var booleanKeyValues = [String: Bool]() func bool(forKey defaultName: String) -> Bool { return booleanKeyValues[defaultName] ?? false } func set(_ value: Bool, forKey defaultName: String) { booleanKeyValues[defaultName] = value } } class SettingsViewControllerTests: XCTestCase { var sut: SettingsViewController! var store: InMemoryStore! override func setUp() { super.setUp() store = InMemoryStore() sut = SettingsViewController(store) } func testSavingTheme() { sut.saveTheme(isDark: true) XCTAssertTrue(store.bool(forKey: darkTheme)) } }
  • 36. Types of dependency injection • Initialiser injection (already saw this) • Property injection • Parameter injection
  • 37. Property injection func testFoo() { let sut = SUT() let collab = Collab() sut.collab = collab let expectedResult = sut.foo() XCTassertTrue(expectedResult) }
  • 38. Parameter injection func testFoo() { let sut = SUT() let collab = Collab() let expectedResult = sut.foo(collab) XCTassertTrue(expectedResult) }
  • 40. Dummy An object we just want to pass as an argument but don't care about how it's used. Usually it returns nil or does nothing in it's methods' implementations class DummyPersonManager: PersonManager { func findPerson(withName name: String) -> String? { return nil } func store(_ person: Person) {} }
  • 41. Fake Same behaviour with the collaborator but lighter implementation without unwanted sidefects Example: InMemoryStore we saw earlier
  • 42. Stub An object whose method's return values we control because the SUT uses it and we want to be able to predict the result of a unit test. struct AlwaysValidValidatorStub: FormDataValidator { func isValid(_ data: FormData) -> Bool { return true } } // configurable across different tests struct ValidatorStub: FormDataValidator { var isValid: Bool func isValid(_ data: FormData) -> Bool { return isValid } }
  • 43. Spy ! A Stub that can also track which of it's methods where called, with what parameters, how many times, etc. struct AlwaysValidValidatorSpy: FormDataValidator { var isValidCallParameters: [FormData] = [] var isValidCalled { return !isValidCallParameters.isEmpty } func isValid(_ data: FormData) -> Bool { isValidCallParameters.append(data) return true } }
  • 44. Mock A Spy that can also verify that a specific set of preconditions have happened struct ChrisLattnerValidatorMock: FormDataValidator { var isValidCallParameters: [FormData] = [] var isValidCalled { return !isValidCallParameters.isEmpty } func isValid(_ data: FormData) -> Bool { isValidCallParameters.append(data) return true } func validate() -> Bool { return isValidCallParameters.map { $0.name }.contains("Chris Lattner") } }
  • 45. Test Driven Developent (TDD) Software development process relying on unit tests and on the RED -> GREEN -> REFACTOR cycle
  • 47. The 3 laws of TDD • You are not allowed to write any production code unless it is to make a failing unit test pass. • You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failures. • You are not allowed to write any more production code than is sufficient to pass the one failing unit test.
  • 48. Example: Story point calculator
  • 49. Make it work, make it right, make it fast. — Kent Beck
  • 51. TDD benefits 1. Breaks complex problems into small simple problems you can focus on -> confidence when writing new code 2. Tests serve as up to date documentation 3. writing test first makes production code testable / losely coupled 4. Reduced debugging (localized test failures in the code modified since last success) 5. Refactor more frequently and without fear (in every cycle) 6. Forces us to raise unit coverage
  • 52. Unit testing tips and tricks
  • 53. Use Xcode keyboard shortcuts for testing • ⌘ U Runs all tests • ctrl ⌥ ⌘ U Runs the current test method • ctrl ⌥ ⌘ G Re-runs the last run • ctrl ⌘ U Run all tests without building
  • 54. Enable sounds for success / failure
  • 55. - Execute in parallel - Randomize execution order
  • 57. To mock or not to mock?
  • 58. Mock a collaborator if • it is slow (e.g. network, db, ui) • it's methods have undeterministic results (e.g. network, db, current time) • it's uninspectable (analytics) • it's methods heve deterministic but difficult to predict results (high cyclomatic complexity)
  • 59. Testing SUT without mocking • many codepaths • complex Arrange • code losely coupled with tests
  • 60. Mock collabs | test collabs separately • minimum codepaths • easier arrange step • tests tightly coupled with code • more fragile tests
  • 61. struct SUT { let collab1: Collab // would need 2*50 = 100 tests to test everything through the SUT without mocking collaborator // needs 2 tests for testing this function if collaborator is mocked + 50 tests for the collaborator func doSth(baz: Bool) -> Int { if baz { return collab.calculate() // 1 } else { return collab.calculate() // 2 } } } struct Collab { let a: A // would need 50 tests func calculate() -> Int { switch a.value { case 1: return 99 case 2: return 100 case 3: return 12 // . // . default: // case 50 return 50 } } }
  • 68. Use factories for creating complex collaborators if you don't mock them The problem Deep dependency graph + Swift type safety = Arrange step hell !
  • 70. func testCanNotifyIfPersonHasValidEmailOrIsNearby() { let sut = Notifier(location: .athensOffice) let address = Address(city: "", country: "", street: "", number: "", postCode: "", floor: 4, coords: CLLocationCoordinate2D(latitude: 37.988067, longitude: 23.734227)) let kostas = Person(id: 0, firstName: "", lastName: "", address: address, email: "krem@mailcom", phone: 0) XCTAssertTrue(sut.canNotify(kostas)) }
  • 73. Seems legit! func testCanNotifyIfPersonHasValidEmailOrIsNearby() { let kostas = Person.fromJSONFile(withName: "kostas") XCTAssertTrue(sut.canNotify(kostas)) } But! • No way to tell from the unit test which parts of the person object are being used by the tested mehtod • JSON == no type safety -> Each time the collaborator type changes you have to manually edit countless JSON files
  • 74. We want to go from func testCanNotifyIfPersonHasValidEmailOrIsNearby() { let sut = Notifier(location: .athensOffice) let address = Address(city: "", country: "", street: "", number: "", postCode: "", floor: 4, coords: CLLocationCoordinate2D(latitude: 37.98, longitude: 23.73)) let kostas = Person(id: 0, firstName: "", lastName: "", address: address, email: "krem@mailcom", phone: 0) XCTAssertTrue(sut.canNotify(kostas)) }
  • 75. To func testCanNotifyIfPersonHasValidEmailOrIsNearby() { let sut = Notifier(location: .athensOffice) let kostas = Person.make( address: .make(coords: CLLocationCoordinate2D(latitude: 37.98, longitude: 23.73)), email: "") XCTAssertTrue(sut.canNotify(kostas)) } What we need is initializers or static factory methods with default values for all parameters so that we can provide only the ones we need for our test.
  • 77. Using Sourcery templates Generated code // in your test target extension Person { static func make(id: Int = Int.defaultValue, firstName: String = String.defaultValue, lastName: String = String.defaultValue, address: Address = Address.make(), email: String = String.defaultValue, phone: Int = Int.defaultValue) -> Person { return self.init(id: id, firstName: firstName, lastName: lastName, address: address, email: email, phone: phone) } } extension Address { static func make(city: String = String.defaultValue, country: String = String.defaultValue, street: String = String.defaultValue, number: String = String.defaultValue, postCode: String = String.defaultValue, floor: Int = Int.defaultValue, coords: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude: 0, longitude: 0)) -> Address { return self.init(city: city, country: country, street: street, number: number, postCode: postCode, floor: floor, coords: coords) } }
  • 78. TDD + Playgrounds = • How to do it • Faster feedback ! • Playgrounds are still unstable ☹ • Diffucult to use dependencies • Which pushes you to think twice about design • You can also use TestDrive to easily use pods • No full inline failure explanation (only in logs)
  • 79. Behavior-Driven Development • Uses a DSL and human language sentences • Closer to user story specs • Given When Then structure • Nesting contexts -> keeps tests dry
  • 81. Take small steps! • Remember the 3 rules of TDD. • Stay GREEN as much as possible • Refactoring == changing code - not behaviour while GREEN
  • 83. legacy code is simply code without tests — Michael Feathers Working Effectively with Legacy Code
  • 87. Resources on Testing / TDD • StoryPointCalculator Demo project • AutoMake sourcery template • Quality coding blog • GeePaw Blog • Refactoring: Improving the Design of Existing Code • Working Effectively with Legacy Code • Swift by Sundell blog- Testing category • BDD with Quick and Nimble