SlideShare a Scribd company logo
1 of 87
Download to read offline
Testing the waters of
iOS
Kostas Kremizas
Twitter: @kostaskremizas
The ONE TRUE WAY™ to write code
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)
What should I test?
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
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
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
Dependency graph
Mock indirect collaborator
!
Mock direct collaborator
!
Use factories
for creating complex collaborators if you don't mock them
The problem
Deep dependency graph + Swift type safety = Arrange step
hell
!
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))
}
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
legacy code is simply
code without tests
— Michael Feathers
Working Effectively with Legacy Code
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

What's hot

What's hot (11)

JQuery selectors
JQuery selectors JQuery selectors
JQuery selectors
 
[232] TensorRT를 활용한 딥러닝 Inference 최적화
[232] TensorRT를 활용한 딥러닝 Inference 최적화[232] TensorRT를 활용한 딥러닝 Inference 최적화
[232] TensorRT를 활용한 딥러닝 Inference 최적화
 
Manual de uso en español sobre Elgg
Manual de uso en español sobre ElggManual de uso en español sobre Elgg
Manual de uso en español sobre Elgg
 
Web Service API Odoo - android
Web Service API Odoo - androidWeb Service API Odoo - android
Web Service API Odoo - android
 
Examen principal- php - correction
Examen principal- php - correctionExamen principal- php - correction
Examen principal- php - correction
 
Examen principal - PHP
Examen principal - PHPExamen principal - PHP
Examen principal - PHP
 
Langage C#
Langage C#Langage C#
Langage C#
 
Google Maps API - DevFest Karlsruhe
Google Maps API - DevFest Karlsruhe Google Maps API - DevFest Karlsruhe
Google Maps API - DevFest Karlsruhe
 
Les nouveautés de Java 21 - Devoxx MA 2023.pdf
Les nouveautés de Java 21 - Devoxx MA 2023.pdfLes nouveautés de Java 21 - Devoxx MA 2023.pdf
Les nouveautés de Java 21 - Devoxx MA 2023.pdf
 
Angular 14.pptx
Angular 14.pptxAngular 14.pptx
Angular 14.pptx
 
Design patterns french
Design patterns frenchDesign patterns french
Design patterns french
 

Similar to Testing the waters of iOS

Unit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMockUnit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMock
Robot Media
 
33rd Degree 2013, Bad Tests, Good Tests
33rd Degree 2013, Bad Tests, Good Tests33rd Degree 2013, Bad Tests, Good Tests
33rd Degree 2013, Bad Tests, Good Tests
Tomek Kaczanowski
 

Similar to Testing the waters of iOS (20)

Kill the mutants - A better way to test your tests
Kill the mutants - A better way to test your testsKill the mutants - A better way to test your tests
Kill the mutants - A better way to test your tests
 
Kill the mutants and test your tests - Roy van Rijn
Kill the mutants and test your tests - Roy van RijnKill the mutants and test your tests - Roy van Rijn
Kill the mutants and test your tests - Roy van Rijn
 
Test driven development
Test driven developmentTest driven development
Test driven development
 
We Are All Testers Now: The Testing Pyramid and Front-End Development
We Are All Testers Now: The Testing Pyramid and Front-End DevelopmentWe Are All Testers Now: The Testing Pyramid and Front-End Development
We Are All Testers Now: The Testing Pyramid and Front-End Development
 
2. overview of c#
2. overview of c#2. overview of c#
2. overview of c#
 
Unit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMockUnit testing in iOS featuring OCUnit, GHUnit & OCMock
Unit testing in iOS featuring OCUnit, GHUnit & OCMock
 
Unit testing patterns for concurrent code
Unit testing patterns for concurrent codeUnit testing patterns for concurrent code
Unit testing patterns for concurrent code
 
Unit Testing: Special Cases
Unit Testing: Special CasesUnit Testing: Special Cases
Unit Testing: Special Cases
 
Building unit tests correctly with visual studio 2013
Building unit tests correctly with visual studio 2013Building unit tests correctly with visual studio 2013
Building unit tests correctly with visual studio 2013
 
Easy mock
Easy mockEasy mock
Easy mock
 
33rd Degree 2013, Bad Tests, Good Tests
33rd Degree 2013, Bad Tests, Good Tests33rd Degree 2013, Bad Tests, Good Tests
33rd Degree 2013, Bad Tests, Good Tests
 
Java Unit Test - JUnit
Java Unit Test - JUnitJava Unit Test - JUnit
Java Unit Test - JUnit
 
2012 JDays Bad Tests Good Tests
2012 JDays Bad Tests Good Tests2012 JDays Bad Tests Good Tests
2012 JDays Bad Tests Good Tests
 
Java Tutorial | My Heart
Java Tutorial | My HeartJava Tutorial | My Heart
Java Tutorial | My Heart
 
Synapseindia reviews.odp.
Synapseindia reviews.odp.Synapseindia reviews.odp.
Synapseindia reviews.odp.
 
Java tut1
Java tut1Java tut1
Java tut1
 
Java Tutorial
Java TutorialJava Tutorial
Java Tutorial
 
Tutorial java
Tutorial javaTutorial java
Tutorial java
 
Java Tut1
Java Tut1Java Tut1
Java Tut1
 
Qt test framework
Qt test frameworkQt test framework
Qt test framework
 

Recently uploaded

Easier, Faster, and More Powerful – Alles Neu macht der Mai -Wir durchleuchte...
Easier, Faster, and More Powerful – Alles Neu macht der Mai -Wir durchleuchte...Easier, Faster, and More Powerful – Alles Neu macht der Mai -Wir durchleuchte...
Easier, Faster, and More Powerful – Alles Neu macht der Mai -Wir durchleuchte...
panagenda
 

Recently uploaded (20)

Linux Foundation Edge _ Overview of FDO Software Components _ Randy at Intel.pdf
Linux Foundation Edge _ Overview of FDO Software Components _ Randy at Intel.pdfLinux Foundation Edge _ Overview of FDO Software Components _ Randy at Intel.pdf
Linux Foundation Edge _ Overview of FDO Software Components _ Randy at Intel.pdf
 
Where to Learn More About FDO _ Richard at FIDO Alliance.pdf
Where to Learn More About FDO _ Richard at FIDO Alliance.pdfWhere to Learn More About FDO _ Richard at FIDO Alliance.pdf
Where to Learn More About FDO _ Richard at FIDO Alliance.pdf
 
Google I/O Extended 2024 Warsaw
Google I/O Extended 2024 WarsawGoogle I/O Extended 2024 Warsaw
Google I/O Extended 2024 Warsaw
 
Optimizing NoSQL Performance Through Observability
Optimizing NoSQL Performance Through ObservabilityOptimizing NoSQL Performance Through Observability
Optimizing NoSQL Performance Through Observability
 
Simplified FDO Manufacturing Flow with TPMs _ Liam at Infineon.pdf
Simplified FDO Manufacturing Flow with TPMs _ Liam at Infineon.pdfSimplified FDO Manufacturing Flow with TPMs _ Liam at Infineon.pdf
Simplified FDO Manufacturing Flow with TPMs _ Liam at Infineon.pdf
 
Unpacking Value Delivery - Agile Oxford Meetup - May 2024.pptx
Unpacking Value Delivery - Agile Oxford Meetup - May 2024.pptxUnpacking Value Delivery - Agile Oxford Meetup - May 2024.pptx
Unpacking Value Delivery - Agile Oxford Meetup - May 2024.pptx
 
Enterprise Knowledge Graphs - Data Summit 2024
Enterprise Knowledge Graphs - Data Summit 2024Enterprise Knowledge Graphs - Data Summit 2024
Enterprise Knowledge Graphs - Data Summit 2024
 
Demystifying gRPC in .Net by John Staveley
Demystifying gRPC in .Net by John StaveleyDemystifying gRPC in .Net by John Staveley
Demystifying gRPC in .Net by John Staveley
 
Introduction to FDO and How It works Applications _ Richard at FIDO Alliance.pdf
Introduction to FDO and How It works Applications _ Richard at FIDO Alliance.pdfIntroduction to FDO and How It works Applications _ Richard at FIDO Alliance.pdf
Introduction to FDO and How It works Applications _ Richard at FIDO Alliance.pdf
 
WebAssembly is Key to Better LLM Performance
WebAssembly is Key to Better LLM PerformanceWebAssembly is Key to Better LLM Performance
WebAssembly is Key to Better LLM Performance
 
ECS 2024 Teams Premium - Pretty Secure
ECS 2024   Teams Premium - Pretty SecureECS 2024   Teams Premium - Pretty Secure
ECS 2024 Teams Premium - Pretty Secure
 
AI revolution and Salesforce, Jiří Karpíšek
AI revolution and Salesforce, Jiří KarpíšekAI revolution and Salesforce, Jiří Karpíšek
AI revolution and Salesforce, Jiří Karpíšek
 
Microsoft CSP Briefing Pre-Engagement - Questionnaire
Microsoft CSP Briefing Pre-Engagement - QuestionnaireMicrosoft CSP Briefing Pre-Engagement - Questionnaire
Microsoft CSP Briefing Pre-Engagement - Questionnaire
 
Using IESVE for Room Loads Analysis - UK & Ireland
Using IESVE for Room Loads Analysis - UK & IrelandUsing IESVE for Room Loads Analysis - UK & Ireland
Using IESVE for Room Loads Analysis - UK & Ireland
 
Choosing the Right FDO Deployment Model for Your Application _ Geoffrey at In...
Choosing the Right FDO Deployment Model for Your Application _ Geoffrey at In...Choosing the Right FDO Deployment Model for Your Application _ Geoffrey at In...
Choosing the Right FDO Deployment Model for Your Application _ Geoffrey at In...
 
Easier, Faster, and More Powerful – Notes Document Properties Reimagined
Easier, Faster, and More Powerful – Notes Document Properties ReimaginedEasier, Faster, and More Powerful – Notes Document Properties Reimagined
Easier, Faster, and More Powerful – Notes Document Properties Reimagined
 
The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdfThe Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
The Value of Certifying Products for FDO _ Paul at FIDO Alliance.pdf
 
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
Measures in SQL (a talk at SF Distributed Systems meetup, 2024-05-22)
 
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdfHow Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
How Red Hat Uses FDO in Device Lifecycle _ Costin and Vitaliy at Red Hat.pdf
 
Easier, Faster, and More Powerful – Alles Neu macht der Mai -Wir durchleuchte...
Easier, Faster, and More Powerful – Alles Neu macht der Mai -Wir durchleuchte...Easier, Faster, and More Powerful – Alles Neu macht der Mai -Wir durchleuchte...
Easier, Faster, and More Powerful – Alles Neu macht der Mai -Wir durchleuchte...
 

Testing the waters of iOS

  • 1. Testing the waters of iOS Kostas Kremizas Twitter: @kostaskremizas
  • 2.
  • 3.
  • 4.
  • 5. The ONE TRUE WAY™ to write code
  • 6.
  • 7.
  • 8.
  • 9.
  • 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)
  • 15.
  • 16. What should I test?
  • 17.
  • 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
  • 46.
  • 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
  • 50.
  • 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 } } }
  • 63.
  • 67.
  • 68. Use factories for creating complex collaborators if you don't mock them The problem Deep dependency graph + Swift type safety = Arrange step hell !
  • 69.
  • 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)) }
  • 71.
  • 72.
  • 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
  • 82.
  • 83. legacy code is simply code without tests — Michael Feathers Working Effectively with Legacy Code
  • 84.
  • 85.
  • 86.
  • 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