SlideShare uma empresa Scribd logo
1 de 33
Baixar para ler offline
Writing Swift code
with great testability
⌘ U
+how became my new ⌘ R
+
@johnsundell
or,
⌘ U
+how became my new ⌘ R
+
Unit tests are a waste of time. It’s faster and easier to do
manual testing, and focus my coding time on writing
actual code.
Unbox, Wrap, Hub Framework, etc…
Integrating, re-building, manual testing⏳
Bugs, regressions, unwanted behavior"
⌘ U
+how became my new ⌘ R
+
Automating testing = Focus on coding#
$
Spotify app
Hub Framework 20x faster compile times!
Tests provide documentation of intent%
Tests let me move faster, I don’t have to constantly run
the app, but can instead verify most of its working parts
in isolation, and make quick iterations.
3 tips on how to work
with unit testing in
1 Design your code for testability
What makes code easy to test?
Unified
Input/Output
Injected
dependencies
State is kept
local
1 Design your code for testability
class FileLoader {
static let shared = FileLoader()
private let cache = Cache()
func file(named fileName: String) throws -> File {
if let cachedFile = cache.file(named: fileName) {
return cachedFile
}
let bundle = Bundle.main
guard let url = bundle.url(forResource: fileName, withExtension: nil) else {
throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil)
}
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
}
}
Unified
Input/Output
Injected
dependencies
State is kept
local
class FileLoader {
static let shared = FileLoader()
private let cache = Cache()
func file(named fileName: String) throws -> File {
if let cachedFile = cache.file(named: fileName) {
return cachedFile
}
let bundle = Bundle.main
guard let url = bundle.url(forResource: fileName, withExtension: nil) else {
throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil)
}
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
}
}
Unified
Input/Output
Injected
dependencies
State is kept
local
Unified
Input/Output
class FileLoader {
static let shared = FileLoader()
private let cache = Cache()
func file(named fileName: String) throws -> File {
if let cachedFile = cache.file(named: fileName) {
return cachedFile
}
let bundle = Bundle.main
guard let url = bundle.url(forResource: fileName, withExtension: nil) else {
throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil)
}
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
}
}
Unified
Input/Output
Injected
dependencies
State is kept
local
Unified
Input/Output
State is kept
local
class FileLoader {
static let shared = FileLoader()
private let cache = Cache()
func file(named fileName: String) throws -> File {
if let cachedFile = cache.file(named: fileName) {
return cachedFile
}
let bundle = Bundle.main
guard let url = bundle.url(forResource: fileName, withExtension: nil) else {
throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil)
}
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
}
}
Unified
Input/Output
Injected
dependencies
State is kept
local
Unified
Input/Output
State is kept
local
Injected
dependencies
class FileLoader {
static let shared = FileLoader()
private let cache = Cache()
func file(named fileName: String) throws -> File {
if let cachedFile = cache.file(named: fileName) {
return cachedFile
}
let bundle = Bundle.main
guard let url = bundle.url(forResource: fileName, withExtension: nil) else {
throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil)
}
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
}
}
Unified
Input/Output
Injected
dependencies
State is kept
local
Unified
Input/Output
State is kept
local
Injected
dependencies
enum FileLoaderError: Error {
case invalidFileName(String)
case invalidFileURL(URL)
}
Dedicated error type
throw FileLoaderError.invalidFileName(fileName)
do {
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
} catch {
throw FileLoaderError.invalidFileURL(url)
}
}
}
Unified error output
Unified
Input/Output
class FileLoader {
static let shared = FileLoader()
private let cache = Cache()
func file(named fileName: String) throws -> File {
if let cachedFile = cache.file(named: fileName) {
return cachedFile
}
let bundle = Bundle.main
guard let url = bundle.url(forResource: fileName, withExtension: nil) else {
throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil)
}
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
}
}
Unified
Input/Output
Injected
dependencies
State is kept
local
Unified
Input/Output
State is kept
local
Injected
dependencies
enum FileLoaderError: Error {
case invalidFileName(String)
case invalidFileURL(URL)
}
throw FileLoaderError.invalidFileName(fileName)
do {
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
} catch {
throw FileLoaderError.invalidFileURL(url)
}
}
}
Unified
Input/Output
State is kept
local
class FileLoader {
private let cache = Cache()
Unified
Input/Output
Injected
dependencies
State is kept
local
Unified
Input/Output
State is kept
local
Injected
dependencies
Unified
Input/Output
State is kept
local
private let bundle: Bundle
init(cache: Cache
self.cache = cache
self.bundle = bundle
}
, bundle: Bundle) {
func file(named fileName: String) throws -> File {
if let cachedFile = cache.file(named: fileName) {
return cachedFile
}
let bundle = Bundle.main
guard let url = bundle.url(forResource: fileName, withExtension: nil) else {
throw FileLoaderError.invalidFileName(fileName)
}
do {
let data = try Data(contentsOf: url)
let file = File(data: data)
cache.cache(file: file, name: fileName)
return file
} catch {
throw FileLoaderError.invalidFileURL(url)
}
}
}
Dependency injection (with defaults)
= .init(), bundle: Bundle = .main) {
bundle
Using injected dependencies
Injected
dependencies
Let’s write a test! &
1 Design your code for testability
2 Use access control to create
clear API boundaries
2 Use access control to create
clear API boundaries
APIInput Asserts
Unit test
Code
Integration test
2 Use access control to create
clear API boundaries
private fileprivate internal public open
2 Use access control to create
clear API boundaries
public class SendMessageViewController: UIViewController {
public var recipients: [User]
public var title: String
public var message: String
public var recipientsPicker: UserPickerView?
public var titleTextField: UITextField?
public var messageTextField: UITextField?
}
2 Use access control to create
clear API boundaries
public class SendMessageViewController: UIViewController {
private var recipients: [User]
private var title: String
private var message: String
private var recipientsPicker: UserPickerView?
private var titleTextField: UITextField?
private var messageTextField: UITextField?
}
2 Use access control to create
clear API boundaries
}
func update(recepients: [User]? = nil, title: String? = nil, message: String? = nil) {
if let recepients = recepients {
self.recipients = recepients
}
// Same for title & message
}
public class SendMessageViewController: UIViewController {
private var recipients: [User]
private var title: String
private var message: String
private var recipientsPicker: UserPickerView?
private var titleTextField: UITextField?
private var messageTextField: UITextField? Single API entry point
2 Use access control to create
clear API boundaries
Frameworks
2 Use access control to create
clear API boundaries
Input Asserts
Unit test
Integration test
2 Use access control to create
clear API boundaries
App
Models
Views
Logic
2 Use access control to create
clear API boundaries
App
Models
Views
LogicKit
Kit
($ brew install swiftplate)
3 Avoid mocks to avoid getting tied down
into implementation details
3 Avoid mocks to avoid getting tied down
into implementation details
' Mocks are “fake” objects that are used in tests to be
able to assert that certain things happen as expected.
// Objective-C (using OCMockito)
NSBundle *bundle = mock([NSBundle class]);
[given([bundle pathForResource:anything() ofType:anything()]) willReturn:@“path”];
FileLoader *fileLoader = [[FileLoader alloc] initWithBundle:bundle];
XCTAssertNotNil([fileLoader fileNamed:@"file"]);
// Swift (partial mocking)
class MockBundle: Bundle {
var mockPath: String?
override func path(forResource name: String?, ofType ext: String?) -> String? {
return mockPath
}
}
let bundle = MockBundle()
bundle.mockPath = "path"
let fileLoader = FileLoader(bundle: bundle)
XCTAssertNotNil(fileLoader.file(named: "file"))
// Swift (no mocking)
let bundle = Bundle(for: type(of: self))
let fileLoader = FileLoader(bundle: bundle)
XCTAssertNotNil(fileLoader.file(named: "file"))
3 Avoid mocks to avoid getting tied down
into implementation details
class ImageLoader {
func loadImage(named imageName: String) -> UIImage? {
return UIImage(named: imageName)
}
}
class ImageViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
imageView.image = imageLoader.loadImage(named: imageName)
}
}
// Test using mocking
class ImageViewControllerTests: XCTestCase {
func testImageLoadedOnViewWillAppear() {
class MockImageLoader: ImageLoader {
private(set) var loadedImageNames = [String]()
override func loadImage(named name: String) -> UIImage {
loadedImageNames.append(name)
return UIImage()
}
}
let imageLoader = MockImageLoader()
let vc = ImageViewController(imageLoader: imageLoader)
vc.imageName = "image"
vc.viewWillAppear(false)
XCTAssertEqual(imageLoader.loadedImageNames, ["image"])
}
}
Manually implemented, partial mock
Asserting that an image was loaded by verifying
what our mock captured
Capture loaded image names
3 Avoid mocks to avoid getting tied down
into implementation details
class ImageLoader {
private let preloadedImages: [String : UIImage]
init(preloadedImages: [String : UIImage] = [:]) {
self.preloadedImages = preloadedImages
}
func loadImage(named imageName: String) -> UIImage? {
if let preloadedImage = preloadedImages[imageName] {
return preloadedImage
}
return UIImage(named: imageName)
}
}
// Test without mocking
class ImageViewControllerTests: XCTestCase {
func testImageLoadedOnViewWillAppear() {
let image = UIImage()
let imageLoader = ImageLoader(images: ["image" : image])
let vc = ImageViewController(imageLoader: imageLoader)
vc.imageName = "image"
vc.viewWillAppear(false)
XCTAssertEqual(vc.image, image)
}
}
Optionally enable preloaded images
to be injected
Use preloaded image if any exists
Compare against actually rendered image,
instead of relying on mock capturing
3 Avoid mocks to avoid getting tied down
into implementation details
However, sometimes you do
need mocks, so let’s make it
easy to use them! (
3 Avoid mocks to avoid getting tied down
into implementation details
To summarize
1 Design your code for testability
2 Use access control to create
clear API boundaries
3 Avoid mocks to avoid getting tied down
into implementation details
No tests? )
No problem! *
Just start somewhere (
Set goals for test coverage &
@johnsundell
/johnsundell

Mais conteúdo relacionado

Mais procurados

Iss letcure 7_8
Iss letcure 7_8Iss letcure 7_8
Iss letcure 7_8
Ali Habeeb
 
Grails/Groovyによる開発事例紹介
Grails/Groovyによる開発事例紹介Grails/Groovyによる開発事例紹介
Grails/Groovyによる開発事例紹介
Kiyotaka Oku
 

Mais procurados (19)

groovy & grails - lecture 2
groovy & grails - lecture 2groovy & grails - lecture 2
groovy & grails - lecture 2
 
Dpilot Source Code With ScreenShots
Dpilot Source Code With ScreenShots Dpilot Source Code With ScreenShots
Dpilot Source Code With ScreenShots
 
Source Code for Dpilot
Source Code for Dpilot Source Code for Dpilot
Source Code for Dpilot
 
Testing Web Applications with GEB
Testing Web Applications with GEBTesting Web Applications with GEB
Testing Web Applications with GEB
 
Webinar: MongoDB Persistence with Java and Morphia
Webinar: MongoDB Persistence with Java and MorphiaWebinar: MongoDB Persistence with Java and Morphia
Webinar: MongoDB Persistence with Java and Morphia
 
Reactive Web - Servlet & Async, Non-blocking I/O
Reactive Web - Servlet & Async, Non-blocking I/OReactive Web - Servlet & Async, Non-blocking I/O
Reactive Web - Servlet & Async, Non-blocking I/O
 
Biopython
BiopythonBiopython
Biopython
 
Backbone.js: Run your Application Inside The Browser
Backbone.js: Run your Application Inside The BrowserBackbone.js: Run your Application Inside The Browser
Backbone.js: Run your Application Inside The Browser
 
Using Fuzzy Code Search to Link Code Fragments in Discussions to Source Code
Using Fuzzy Code Search to Link Code Fragments in Discussions to Source CodeUsing Fuzzy Code Search to Link Code Fragments in Discussions to Source Code
Using Fuzzy Code Search to Link Code Fragments in Discussions to Source Code
 
Iss letcure 7_8
Iss letcure 7_8Iss letcure 7_8
Iss letcure 7_8
 
What's new in Liferay Mobile SDK 2.0 for Android
What's new in Liferay Mobile SDK 2.0 for AndroidWhat's new in Liferay Mobile SDK 2.0 for Android
What's new in Liferay Mobile SDK 2.0 for Android
 
Tomcat连接池配置方法V2.1
Tomcat连接池配置方法V2.1Tomcat连接池配置方法V2.1
Tomcat连接池配置方法V2.1
 
Grails/Groovyによる開発事例紹介
Grails/Groovyによる開発事例紹介Grails/Groovyによる開発事例紹介
Grails/Groovyによる開発事例紹介
 
Spock: A Highly Logical Way To Test
Spock: A Highly Logical Way To TestSpock: A Highly Logical Way To Test
Spock: A Highly Logical Way To Test
 
Finding Clojure
Finding ClojureFinding Clojure
Finding Clojure
 
Lab4
Lab4Lab4
Lab4
 
13 networking, mobile services, and authentication
13   networking, mobile services, and authentication13   networking, mobile services, and authentication
13 networking, mobile services, and authentication
 
NIO.2, the I/O API for the future
NIO.2, the I/O API for the futureNIO.2, the I/O API for the future
NIO.2, the I/O API for the future
 
神に近づくx/net/context (Finding God with x/net/context)
神に近づくx/net/context (Finding God with x/net/context)神に近づくx/net/context (Finding God with x/net/context)
神に近づくx/net/context (Finding God with x/net/context)
 

Destaque

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
 
Representation of nationality in captain america
Representation of nationality  in captain americaRepresentation of nationality  in captain america
Representation of nationality in captain america
ddoggart
 
Denis Lebedev, Swift
Denis  Lebedev, SwiftDenis  Lebedev, Swift
Denis Lebedev, Swift
Yandex
 

Destaque (20)

Component-driven UIs - Mobile Era 2016
Component-driven UIs - Mobile Era 2016Component-driven UIs - Mobile Era 2016
Component-driven UIs - Mobile Era 2016
 
Backend-driven native UIs
Backend-driven native UIsBackend-driven native UIs
Backend-driven native UIs
 
Making Swift even safer
Making Swift even saferMaking Swift even safer
Making Swift even safer
 
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
 
Dynamic, native, backend-driven UIs - App Builders 2016
Dynamic, native, backend-driven UIs - App Builders 2016Dynamic, native, backend-driven UIs - App Builders 2016
Dynamic, native, backend-driven UIs - App Builders 2016
 
Building component-driven UIs at Spotify
Building component-driven UIs at SpotifyBuilding component-driven UIs at Spotify
Building component-driven UIs at Spotify
 
How To Build iOS Apps Without interface Builder
How To Build iOS Apps Without interface BuilderHow To Build iOS Apps Without interface Builder
How To Build iOS Apps Without interface Builder
 
I017345357
I017345357I017345357
I017345357
 
196309903 q-answer
196309903 q-answer196309903 q-answer
196309903 q-answer
 
Evaluation question 7
Evaluation question 7Evaluation question 7
Evaluation question 7
 
education people in our society
education people in our society education people in our society
education people in our society
 
Photos lar 2
Photos lar 2Photos lar 2
Photos lar 2
 
Mocking In Swift
Mocking In SwiftMocking In Swift
Mocking In Swift
 
Tdd
TddTdd
Tdd
 
Презентация ученика 6 класса "Дед Мороз и его двойники".
Презентация ученика 6 класса "Дед Мороз и его двойники".Презентация ученика 6 класса "Дед Мороз и его двойники".
Презентация ученика 6 класса "Дед Мороз и его двойники".
 
Trancision 2017
Trancision 2017Trancision 2017
Trancision 2017
 
Representation of nationality in captain america
Representation of nationality  in captain americaRepresentation of nationality  in captain america
Representation of nationality in captain america
 
Denis Lebedev, Swift
Denis  Lebedev, SwiftDenis  Lebedev, Swift
Denis Lebedev, Swift
 
Make Up Tutorials
Make Up TutorialsMake Up Tutorials
Make Up Tutorials
 
UI Testing with Earl Grey
UI Testing with Earl GreyUI Testing with Earl Grey
UI Testing with Earl Grey
 

Semelhante a Writing Swift code with great testability

Refactoring In Tdd The Missing Part
Refactoring In Tdd The Missing PartRefactoring In Tdd The Missing Part
Refactoring In Tdd The Missing Part
Gabriele Lana
 
Gradleintroduction 111010130329-phpapp01
Gradleintroduction 111010130329-phpapp01Gradleintroduction 111010130329-phpapp01
Gradleintroduction 111010130329-phpapp01
Tino Isnich
 
File & Exception Handling in C++.pptx
File & Exception Handling in C++.pptxFile & Exception Handling in C++.pptx
File & Exception Handling in C++.pptx
RutujaTandalwade
 
10 Cool Facts about Gradle
10 Cool Facts about Gradle10 Cool Facts about Gradle
10 Cool Facts about Gradle
Evgeny Goldin
 

Semelhante a Writing Swift code with great testability (20)

Unit testing
Unit testingUnit testing
Unit testing
 
Refactoring In Tdd The Missing Part
Refactoring In Tdd The Missing PartRefactoring In Tdd The Missing Part
Refactoring In Tdd The Missing Part
 
Gradle Introduction
Gradle IntroductionGradle Introduction
Gradle Introduction
 
Gradleintroduction 111010130329-phpapp01
Gradleintroduction 111010130329-phpapp01Gradleintroduction 111010130329-phpapp01
Gradleintroduction 111010130329-phpapp01
 
Input output files in java
Input output files in javaInput output files in java
Input output files in java
 
Introduction to PowerShell
Introduction to PowerShellIntroduction to PowerShell
Introduction to PowerShell
 
Developing IT infrastructures with Puppet
Developing IT infrastructures with PuppetDeveloping IT infrastructures with Puppet
Developing IT infrastructures with Puppet
 
Tornadoweb
TornadowebTornadoweb
Tornadoweb
 
Lambda Chops - Recipes for Simpler, More Expressive Code
Lambda Chops - Recipes for Simpler, More Expressive CodeLambda Chops - Recipes for Simpler, More Expressive Code
Lambda Chops - Recipes for Simpler, More Expressive Code
 
File Handling.pptx
File Handling.pptxFile Handling.pptx
File Handling.pptx
 
Iwatch tech 1
Iwatch tech 1Iwatch tech 1
Iwatch tech 1
 
Grâce aux tags Varnish, j'ai switché ma prod sur Raspberry Pi
Grâce aux tags Varnish, j'ai switché ma prod sur Raspberry PiGrâce aux tags Varnish, j'ai switché ma prod sur Raspberry Pi
Grâce aux tags Varnish, j'ai switché ma prod sur Raspberry Pi
 
Zend framework 03 - singleton factory data mapper caching logging
Zend framework 03 - singleton factory data mapper caching loggingZend framework 03 - singleton factory data mapper caching logging
Zend framework 03 - singleton factory data mapper caching logging
 
Java I/o streams
Java I/o streamsJava I/o streams
Java I/o streams
 
Build powerfull and smart web applications with Symfony2
Build powerfull and smart web applications with Symfony2Build powerfull and smart web applications with Symfony2
Build powerfull and smart web applications with Symfony2
 
DIWE - File handling with PHP
DIWE - File handling with PHPDIWE - File handling with PHP
DIWE - File handling with PHP
 
File & Exception Handling in C++.pptx
File & Exception Handling in C++.pptxFile & Exception Handling in C++.pptx
File & Exception Handling in C++.pptx
 
10 Cool Facts about Gradle
10 Cool Facts about Gradle10 Cool Facts about Gradle
10 Cool Facts about Gradle
 
Hack ASP.NET website
Hack ASP.NET websiteHack ASP.NET website
Hack ASP.NET website
 
hibernate with JPA
hibernate with JPAhibernate with JPA
hibernate with JPA
 

Último

Why Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businessWhy Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire business
panagenda
 
Architecting Cloud Native Applications
Architecting Cloud Native ApplicationsArchitecting Cloud Native Applications
Architecting Cloud Native Applications
WSO2
 

Último (20)

Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...
 
"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
"I see eyes in my soup": How Delivery Hero implemented the safety system for ..."I see eyes in my soup": How Delivery Hero implemented the safety system for ...
"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
 
Why Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire businessWhy Teams call analytics are critical to your entire business
Why Teams call analytics are critical to your entire business
 
Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)Powerful Google developer tools for immediate impact! (2023-24 C)
Powerful Google developer tools for immediate impact! (2023-24 C)
 
Ransomware_Q4_2023. The report. [EN].pdf
Ransomware_Q4_2023. The report. [EN].pdfRansomware_Q4_2023. The report. [EN].pdf
Ransomware_Q4_2023. The report. [EN].pdf
 
Automating Google Workspace (GWS) & more with Apps Script
Automating Google Workspace (GWS) & more with Apps ScriptAutomating Google Workspace (GWS) & more with Apps Script
Automating Google Workspace (GWS) & more with Apps Script
 
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt Robison
 
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
Mastering MySQL Database Architecture: Deep Dive into MySQL Shell and MySQL R...
 
Apidays Singapore 2024 - Scalable LLM APIs for AI and Generative AI Applicati...
Apidays Singapore 2024 - Scalable LLM APIs for AI and Generative AI Applicati...Apidays Singapore 2024 - Scalable LLM APIs for AI and Generative AI Applicati...
Apidays Singapore 2024 - Scalable LLM APIs for AI and Generative AI Applicati...
 
Architecting Cloud Native Applications
Architecting Cloud Native ApplicationsArchitecting Cloud Native Applications
Architecting Cloud Native Applications
 
AXA XL - Insurer Innovation Award Americas 2024
AXA XL - Insurer Innovation Award Americas 2024AXA XL - Insurer Innovation Award Americas 2024
AXA XL - Insurer Innovation Award Americas 2024
 
Emergent Methods: Multi-lingual narrative tracking in the news - real-time ex...
Emergent Methods: Multi-lingual narrative tracking in the news - real-time ex...Emergent Methods: Multi-lingual narrative tracking in the news - real-time ex...
Emergent Methods: Multi-lingual narrative tracking in the news - real-time ex...
 
Artificial Intelligence Chap.5 : Uncertainty
Artificial Intelligence Chap.5 : UncertaintyArtificial Intelligence Chap.5 : Uncertainty
Artificial Intelligence Chap.5 : Uncertainty
 
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, AdobeApidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
Apidays New York 2024 - Scaling API-first by Ian Reasor and Radu Cotescu, Adobe
 
MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024MINDCTI Revenue Release Quarter One 2024
MINDCTI Revenue Release Quarter One 2024
 
EMPOWERMENT TECHNOLOGY GRADE 11 QUARTER 2 REVIEWER
EMPOWERMENT TECHNOLOGY GRADE 11 QUARTER 2 REVIEWEREMPOWERMENT TECHNOLOGY GRADE 11 QUARTER 2 REVIEWER
EMPOWERMENT TECHNOLOGY GRADE 11 QUARTER 2 REVIEWER
 
Corporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptxCorporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptx
 
Strategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a FresherStrategies for Landing an Oracle DBA Job as a Fresher
Strategies for Landing an Oracle DBA Job as a Fresher
 
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
Web Form Automation for Bonterra Impact Management (fka Social Solutions Apri...
 

Writing Swift code with great testability

  • 1. Writing Swift code with great testability ⌘ U +how became my new ⌘ R + @johnsundell or,
  • 2. ⌘ U +how became my new ⌘ R + Unit tests are a waste of time. It’s faster and easier to do manual testing, and focus my coding time on writing actual code. Unbox, Wrap, Hub Framework, etc… Integrating, re-building, manual testing⏳ Bugs, regressions, unwanted behavior"
  • 3. ⌘ U +how became my new ⌘ R + Automating testing = Focus on coding# $ Spotify app Hub Framework 20x faster compile times! Tests provide documentation of intent% Tests let me move faster, I don’t have to constantly run the app, but can instead verify most of its working parts in isolation, and make quick iterations.
  • 4. 3 tips on how to work with unit testing in
  • 5. 1 Design your code for testability
  • 6. What makes code easy to test? Unified Input/Output Injected dependencies State is kept local 1 Design your code for testability
  • 7. class FileLoader { static let shared = FileLoader() private let cache = Cache() func file(named fileName: String) throws -> File { if let cachedFile = cache.file(named: fileName) { return cachedFile } let bundle = Bundle.main guard let url = bundle.url(forResource: fileName, withExtension: nil) else { throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil) } let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } } Unified Input/Output Injected dependencies State is kept local
  • 8. class FileLoader { static let shared = FileLoader() private let cache = Cache() func file(named fileName: String) throws -> File { if let cachedFile = cache.file(named: fileName) { return cachedFile } let bundle = Bundle.main guard let url = bundle.url(forResource: fileName, withExtension: nil) else { throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil) } let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } } Unified Input/Output Injected dependencies State is kept local Unified Input/Output
  • 9. class FileLoader { static let shared = FileLoader() private let cache = Cache() func file(named fileName: String) throws -> File { if let cachedFile = cache.file(named: fileName) { return cachedFile } let bundle = Bundle.main guard let url = bundle.url(forResource: fileName, withExtension: nil) else { throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil) } let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } } Unified Input/Output Injected dependencies State is kept local Unified Input/Output State is kept local
  • 10. class FileLoader { static let shared = FileLoader() private let cache = Cache() func file(named fileName: String) throws -> File { if let cachedFile = cache.file(named: fileName) { return cachedFile } let bundle = Bundle.main guard let url = bundle.url(forResource: fileName, withExtension: nil) else { throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil) } let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } } Unified Input/Output Injected dependencies State is kept local Unified Input/Output State is kept local Injected dependencies
  • 11. class FileLoader { static let shared = FileLoader() private let cache = Cache() func file(named fileName: String) throws -> File { if let cachedFile = cache.file(named: fileName) { return cachedFile } let bundle = Bundle.main guard let url = bundle.url(forResource: fileName, withExtension: nil) else { throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil) } let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } } Unified Input/Output Injected dependencies State is kept local Unified Input/Output State is kept local Injected dependencies enum FileLoaderError: Error { case invalidFileName(String) case invalidFileURL(URL) } Dedicated error type throw FileLoaderError.invalidFileName(fileName) do { let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } catch { throw FileLoaderError.invalidFileURL(url) } } } Unified error output Unified Input/Output
  • 12. class FileLoader { static let shared = FileLoader() private let cache = Cache() func file(named fileName: String) throws -> File { if let cachedFile = cache.file(named: fileName) { return cachedFile } let bundle = Bundle.main guard let url = bundle.url(forResource: fileName, withExtension: nil) else { throw NSError(domain: "com.johnsundell.myapp.fileLoader", code: 7, userInfo: nil) } let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } } Unified Input/Output Injected dependencies State is kept local Unified Input/Output State is kept local Injected dependencies enum FileLoaderError: Error { case invalidFileName(String) case invalidFileURL(URL) } throw FileLoaderError.invalidFileName(fileName) do { let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } catch { throw FileLoaderError.invalidFileURL(url) } } } Unified Input/Output State is kept local
  • 13. class FileLoader { private let cache = Cache() Unified Input/Output Injected dependencies State is kept local Unified Input/Output State is kept local Injected dependencies Unified Input/Output State is kept local private let bundle: Bundle init(cache: Cache self.cache = cache self.bundle = bundle } , bundle: Bundle) { func file(named fileName: String) throws -> File { if let cachedFile = cache.file(named: fileName) { return cachedFile } let bundle = Bundle.main guard let url = bundle.url(forResource: fileName, withExtension: nil) else { throw FileLoaderError.invalidFileName(fileName) } do { let data = try Data(contentsOf: url) let file = File(data: data) cache.cache(file: file, name: fileName) return file } catch { throw FileLoaderError.invalidFileURL(url) } } } Dependency injection (with defaults) = .init(), bundle: Bundle = .main) { bundle Using injected dependencies Injected dependencies
  • 14. Let’s write a test! & 1 Design your code for testability
  • 15. 2 Use access control to create clear API boundaries
  • 16. 2 Use access control to create clear API boundaries APIInput Asserts Unit test Code Integration test
  • 17. 2 Use access control to create clear API boundaries private fileprivate internal public open
  • 18. 2 Use access control to create clear API boundaries public class SendMessageViewController: UIViewController { public var recipients: [User] public var title: String public var message: String public var recipientsPicker: UserPickerView? public var titleTextField: UITextField? public var messageTextField: UITextField? }
  • 19. 2 Use access control to create clear API boundaries public class SendMessageViewController: UIViewController { private var recipients: [User] private var title: String private var message: String private var recipientsPicker: UserPickerView? private var titleTextField: UITextField? private var messageTextField: UITextField? }
  • 20. 2 Use access control to create clear API boundaries } func update(recepients: [User]? = nil, title: String? = nil, message: String? = nil) { if let recepients = recepients { self.recipients = recepients } // Same for title & message } public class SendMessageViewController: UIViewController { private var recipients: [User] private var title: String private var message: String private var recipientsPicker: UserPickerView? private var titleTextField: UITextField? private var messageTextField: UITextField? Single API entry point
  • 21. 2 Use access control to create clear API boundaries Frameworks
  • 22. 2 Use access control to create clear API boundaries Input Asserts Unit test Integration test
  • 23. 2 Use access control to create clear API boundaries App Models Views Logic
  • 24. 2 Use access control to create clear API boundaries App Models Views LogicKit Kit ($ brew install swiftplate)
  • 25. 3 Avoid mocks to avoid getting tied down into implementation details
  • 26. 3 Avoid mocks to avoid getting tied down into implementation details ' Mocks are “fake” objects that are used in tests to be able to assert that certain things happen as expected.
  • 27. // Objective-C (using OCMockito) NSBundle *bundle = mock([NSBundle class]); [given([bundle pathForResource:anything() ofType:anything()]) willReturn:@“path”]; FileLoader *fileLoader = [[FileLoader alloc] initWithBundle:bundle]; XCTAssertNotNil([fileLoader fileNamed:@"file"]); // Swift (partial mocking) class MockBundle: Bundle { var mockPath: String? override func path(forResource name: String?, ofType ext: String?) -> String? { return mockPath } } let bundle = MockBundle() bundle.mockPath = "path" let fileLoader = FileLoader(bundle: bundle) XCTAssertNotNil(fileLoader.file(named: "file")) // Swift (no mocking) let bundle = Bundle(for: type(of: self)) let fileLoader = FileLoader(bundle: bundle) XCTAssertNotNil(fileLoader.file(named: "file")) 3 Avoid mocks to avoid getting tied down into implementation details
  • 28. class ImageLoader { func loadImage(named imageName: String) -> UIImage? { return UIImage(named: imageName) } } class ImageViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) imageView.image = imageLoader.loadImage(named: imageName) } } // Test using mocking class ImageViewControllerTests: XCTestCase { func testImageLoadedOnViewWillAppear() { class MockImageLoader: ImageLoader { private(set) var loadedImageNames = [String]() override func loadImage(named name: String) -> UIImage { loadedImageNames.append(name) return UIImage() } } let imageLoader = MockImageLoader() let vc = ImageViewController(imageLoader: imageLoader) vc.imageName = "image" vc.viewWillAppear(false) XCTAssertEqual(imageLoader.loadedImageNames, ["image"]) } } Manually implemented, partial mock Asserting that an image was loaded by verifying what our mock captured Capture loaded image names 3 Avoid mocks to avoid getting tied down into implementation details
  • 29. class ImageLoader { private let preloadedImages: [String : UIImage] init(preloadedImages: [String : UIImage] = [:]) { self.preloadedImages = preloadedImages } func loadImage(named imageName: String) -> UIImage? { if let preloadedImage = preloadedImages[imageName] { return preloadedImage } return UIImage(named: imageName) } } // Test without mocking class ImageViewControllerTests: XCTestCase { func testImageLoadedOnViewWillAppear() { let image = UIImage() let imageLoader = ImageLoader(images: ["image" : image]) let vc = ImageViewController(imageLoader: imageLoader) vc.imageName = "image" vc.viewWillAppear(false) XCTAssertEqual(vc.image, image) } } Optionally enable preloaded images to be injected Use preloaded image if any exists Compare against actually rendered image, instead of relying on mock capturing 3 Avoid mocks to avoid getting tied down into implementation details
  • 30. However, sometimes you do need mocks, so let’s make it easy to use them! ( 3 Avoid mocks to avoid getting tied down into implementation details
  • 31. To summarize 1 Design your code for testability 2 Use access control to create clear API boundaries 3 Avoid mocks to avoid getting tied down into implementation details
  • 32. No tests? ) No problem! * Just start somewhere ( Set goals for test coverage &