Slide 1

Slide 1 text

The Two Sides of Testing

Slide 2

Slide 2 text

Brandon Williams — [email protected] — @mbrandonw — github.com/kickstarter/ios-oss

Slide 3

Slide 3 text

Why test?

Slide 4

Slide 4 text

A case study

Slide 5

Slide 5 text

/** * Reads a number from a file on disk, performs a computation, prints the result to the console, and returns the result. */ func compute(file: String) -> Int { }

Slide 6

Slide 6 text

/** * Reads a number from a file on disk, performs a computation, prints the result to the console, and returns the result. */ func compute(file: String) -> Int { let value = Bundle.main.path(forResource: file, ofType: nil) .flatMap { try? String(contentsOfFile: $0) } .flatMap { Int($0) } ?? 0 let result = value * value print("Computed: \(result)") return result }

Slide 7

Slide 7 text

/** * Reads a number from a file on disk, performs a computation, prints the result to the console, and returns the result. */ func compute(file: String) -> Int { let value = Bundle.main.path(forResource: file, ofType: nil) // "/var/.../number.txt" .flatMap { try? String(contentsOfFile: $0) } // "123" .flatMap { Int($0) } // 123 ?? 0 // 123 let result = value * value // 15129 print("Computed: \(result)") // "Computed: 15129\n" return result // 15129 } compute(file: "number.txt") // 15129

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

No content

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

Testing output

Slide 16

Slide 16 text

Testing output Side effects

Slide 17

Slide 17 text

Side effects An expression is said to have a “side effect” if its execution makes an observable change to the outside world.

Slide 18

Slide 18 text

How do you test code with side effects?

Slide 19

Slide 19 text

A be!er way to handle side effects

Slide 20

Slide 20 text

A be!er way to handle side effects Try to describe effects as much as possible without actually performing the effects.

Slide 21

Slide 21 text

A be!er way to handle side effects func compute(file: String) -> (Int, String) { let value = Bundle.main.path(forResource: file, ofType: nil) .flatMap { try? String(contentsOfFile: $0) } .flatMap { Int($0) } ?? 0 let result = value * value return (result, "Computed: \(result)") }

Slide 22

Slide 22 text

Testing Input

Slide 23

Slide 23 text

Testing Input Co-effects

Slide 24

Slide 24 text

Testing Input Co-effects i.e. the “dual” of side effects

Slide 25

Slide 25 text

Co-effects If an effect is a change to the outside world after executing an expression... ...then... ...a co-effect is the state of the world that the expression needs in order to execute.

Slide 26

Slide 26 text

Co-effects An expression is said to have a “co-effect” if it requires a particular state of the world in order to execute.

Slide 27

Slide 27 text

How do you test code with co-effects?

Slide 28

Slide 28 text

A be!er way to handle co-effects

Slide 29

Slide 29 text

struct Environment { }

Slide 30

Slide 30 text

struct Environment { let apiService: ServiceProtocol }

Slide 31

Slide 31 text

struct Environment { let apiService: ServiceProtocol let cookieStorage: HTTPCookieStorageProtocol }

Slide 32

Slide 32 text

struct Environment { let apiService: ServiceProtocol let cookieStorage: HTTPCookieStorageProtocol let currentUser: User? }

Slide 33

Slide 33 text

struct Environment { let apiService: ServiceProtocol let cookieStorage: HTTPCookieStorageProtocol let currentUser: User? let dateType: DateProtocol.Type }

Slide 34

Slide 34 text

struct Environment { let apiService: ServiceProtocol let cookieStorage: HTTPCookieStorageProtocol let currentUser: User? let dateType: DateProtocol.Type let language: Language }

Slide 35

Slide 35 text

struct Environment { let apiService: ServiceProtocol let cookieStorage: HTTPCookieStorageProtocol let currentUser: User? let dateType: DateProtocol.Type let language: Language let mainBundle: BundleProtocol }

Slide 36

Slide 36 text

struct Environment { let apiService: ServiceProtocol let cookieStorage: HTTPCookieStorageProtocol let currentUser: User? let dateType: DateProtocol.Type let language: Language let mainBundle: BundleProtocol let reachability: SignalProducer }

Slide 37

Slide 37 text

struct Environment { let apiService: ServiceProtocol let cookieStorage: HTTPCookieStorageProtocol let currentUser: User? let dateType: DateProtocol.Type let language: Language let mainBundle: BundleProtocol let reachability: SignalProducer let scheduler: DateSchedulerProtocol }

Slide 38

Slide 38 text

struct Environment { let apiService: ServiceProtocol let cookieStorage: HTTPCookieStorageProtocol let currentUser: User? let dateType: DateProtocol.Type let language: Language let mainBundle: BundleProtocol let reachability: SignalProducer let scheduler: DateSchedulerProtocol let userDefaults: UserDefaultsProtocol }

Slide 39

Slide 39 text

A be!er way to handle co-effects Refactor

Slide 40

Slide 40 text

Refactor Bundle.main.path(forResource: file, ofType: nil)

Slide 41

Slide 41 text

Refactor protocol BundleProtocol { func path(forResource name: String?, ofType ext: String?) -> String? } extension Bundle: BundleProtocol {}

Slide 42

Slide 42 text

Refactor struct SuccessfulPathForResourceBundle: BundleProtocol { func path(forResource name: String?, ofType ext: String?) -> String? { return "a/path/to/a/file.txt" } } struct FailedPathForResourceBundle: BundleProtocol { func path(forResource name: String?, ofType ext: String?) -> String? { return nil } }

Slide 43

Slide 43 text

Refactor String(contentsOfFile: file)

Slide 44

Slide 44 text

Refactor protocol ContentsOfFileProtocol { static func from(contentsOfFile file: String) throws -> String } extension String: ContentsOfFileProtocol { static func from(contentsOfFile file: String) throws -> String { return try String(contentsOfFile: file) } }

Slide 45

Slide 45 text

Refactor struct IntContentsOfFile: ContentsOfFileProtocol { static func from(contentsOfFile file: String) throws -> String { return "123" } } struct NonIntContentsOfFile: ContentsOfFileProtocol { static func from(contentsOfFile file: String) throws -> String { return "asdf" } } struct ThrowingContentsOfFile: ContentsOfFileProtocol { static func from(contentsOfFile file: String) throws -> String { throw SomeError() } }

Slide 46

Slide 46 text

Refactor func compute(file: String, bundle: BundleProtocol = Bundle.main, contentsOfFileProtocol: ContentsOfFileProtocol.Type = String.self) -> (Int, String) { let value = bundle.path(forResource: file, ofType: nil) .flatMap { try? contentsOfFileProtocol.from(contentsOfFile: $0) } .flatMap { Int($0) } ?? 0 let result = value * value return (result, "Computed: \(result)") }

Slide 47

Slide 47 text

Conclusion

Slide 48

Slide 48 text

Conclusion The two things that make testing difficult are effects and co-effects.

Slide 49

Slide 49 text

Conclusion To tame effects, think of them as data in their own right, and you simply describe the effect rather than actually perform it. A naive interpreter can perform the effects somewhere else.

Slide 50

Slide 50 text

Conclusion To tame co-effects, put them all in one big ole global struct, and don't ever access a global unless it is through that struct.

Slide 51

Slide 51 text

Thanks — [email protected] — @mbrandonw — github.com/kickstarter/ios-oss