Slide 1

Slide 1 text

Building a robust Swift mocking Framework Pawel Dudek

Slide 2

Slide 2 text

2

Slide 3

Slide 3 text

So you wanted to mock types in Swift...

Slide 4

Slide 4 text

The Why

Slide 5

Slide 5 text

Seams

Slide 6

Slide 6 text

enum ReleaseType { case production case debug } class ReleaseTypeService { var releaseType: ReleaseType { switch Bundle.main.bundleIdentifier { case "productionBundleId": return .production case "debugBundleId": return .debug } } } Missing default case on purpose 6

Slide 7

Slide 7 text

enum ReleaseType { case production case debug } class ReleaseTypeService { var releaseType: ReleaseType { switch Bundle.main.bundleIdentifier { case "productionBundleId": return .production case "debugBundleId": return .debug } } } Missing default case on purpose 7

Slide 8

Slide 8 text

protocol BundleIdProvider { var bundleIdentifier: String? { get } } 8

Slide 9

Slide 9 text

protocol BundleIdProvider { var bundleIdentifier: String? { get } } extension Bundle: BundleIdProvider {} 9

Slide 10

Slide 10 text

class ReleaseTypeService { let bundleIdProvider: BundleIdProvider init(bundleIdProvider: BundleIdProvider) { self.bundleIdProvider = bundleIdProvider } var releaseType: ReleaseType { switch bundleIdProvider.bundleIdentifier { case "productionBundleId": return .production case "debugBundleId": return .debug } } } Missing default case on purpose 10

Slide 11

Slide 11 text

class ReleaseTypeService { let bundleIdProvider: BundleIdProvider init(bundleIdProvider: BundleIdProvider) { self.bundleIdProvider = bundleIdProvider } var releaseType: ReleaseType { switch bundleIdProvider.bundleIdentifier { case "productionBundleId": return .production case "debugBundleId": return .debug } } } Missing default case on purpose 11

Slide 12

Slide 12 text

The Journey through Land of Swift Mocks

Slide 13

Slide 13 text

Hand written mocks

Slide 14

Slide 14 text

protocol Foo { func bar() } 14

Slide 15

Slide 15 text

class MockFoo: Foo { var barCalled = false func bar() { barCalled = true } } XCTAssertTrue(mockFoo.barCalled) 15

Slide 16

Slide 16 text

class MockFoo: Foo { var timesBarCalled = 0 func bar() { timesBarCalled += 1 } } XCTAssertEqual(mockFoo.timesBarCalled, 1) 16

Slide 17

Slide 17 text

But what about arguments? protocol ArgumentFoo { func bar(arg1: Int, arg2: UIViewController) } 17

Slide 18

Slide 18 text

class MockArgumentFoo: ArgumentFoo { var timesBarCalled = 0 var barArgments: [(Int, UIViewController)] = [] func bar(arg1: Int, arg2: UIViewController) { timesBarCalled += 1 barArgments.append((arg1, arg2)) } } 18

Slide 19

Slide 19 text

class MockArgumentFoo: ArgumentFoo { var barArgments: [(Int, UIViewController)] = [] func bar(arg1: Int, arg2: UIViewController) { barArgments.append((arg1, arg2)) } } 19

Slide 20

Slide 20 text

class MockArgumentFoo: ArgumentFoo { var barArgments: [(Int, UIViewController)] = [] func bar(arg1: Int, arg2: UIViewController) { barArgments.append((arg1, arg2)) } } XCTAssert // ??? 20

Slide 21

Slide 21 text

OCMockito // mock creation NSMutableArray *mockArray = mock([NSMutableArray class]); // using mock object [mockArray removeObject:@"This is a test"]; // verification [verify(mockArray) removeObject:startsWith(@"This is")]; 21

Slide 22

Slide 22 text

class MockArgumentFoo: ArgumentFoo { var barArgments: [(Int, UIViewController)] = [] func bar(arg1: Int, arg2: UIViewController) { barArgments.append((arg1, arg2)) } var bar2Argments: [(CFTimeInterval, Float)] = [] func bar2(arg1: CFTimeInterval, arg2: Float) { bar2Argments.append((arg1, arg2)) } } 22

Slide 23

Slide 23 text

Function + Arguments = Invocation

Slide 24

Slide 24 text

struct Invocation { let identifier: String let arguments: [Any?]? } 24

Slide 25

Slide 25 text

class Storage { var recordedInvocations: [Invocation] = [] func recordInvocation( withIdentifier identifier: String, arguments: [Any?]? ) { recordedInvocations.append(.init( identifier: identifier, arguments: arguments )) } } 25

Slide 26

Slide 26 text

class StorageMockArgumentFoo: ArgumentFoo { let storage = Storage() func bar(arg1: Int, arg2: UIViewController) { storage.recordInvocation( withIdentifier: "bar", arguments: [arg1, arg2] ) } func bar2(arg1: CFTimeInterval, arg2: Float) { storage.recordInvocation( withIdentifier: "bar2", arguments: [arg1, arg2] ) } } 26

Slide 27

Slide 27 text

Matching arguments

Slide 28

Slide 28 text

let arguments: [Any?]? = [value1, value2] let expectedArguments: [???] = ??? // Won't compile, but we want something similar to this XCTEqual(arguments, expectedArguments) 28

Slide 29

Slide 29 text

Any?

Slide 30

Slide 30 text

Ability to match values against a varienty of different rules

Slide 31

Slide 31 text

protocol Matcher { func matches(argument: Any?) -> Bool } 31

Slide 32

Slide 32 text

class EqualTo: Matcher { private let object: T? init(_ object: T?) { self.object = object } func matches(argument: Any?) -> Bool { if let otherObject = argument as? T { return object == otherObject } return argument == nil && object == nil } } 32

Slide 33

Slide 33 text

class EqualTo: Matcher { private let object: T? init(_ object: T?) { self.object = object } func matches(argument: Any?) -> Bool { if let otherObject = argument as? T { return object == otherObject } return argument == nil && object == nil } } 33

Slide 34

Slide 34 text

class IdenticalTo: Matcher { ... } class InstanceOf: Matcher { ... } class NotMatcher: Matcher { ...} class CaptureArgumentMatcher: Matcher { ... } class AnyMatcher: Matcher {} class SimulateCallbackMatcher: Matcher { ... } 34

Slide 35

Slide 35 text

Usage 35

Slide 36

Slide 36 text

let matchers: [Matcher] = [ EqualTo(Int(42)), IdenticalTo(viewController), ] let arguments: [Any?] = storage.(...) // Missing edge cases like different argument count let match = zip(matchers, arguments) .allSatisfy { $0.matches(argument: $1) } XCTAssertTrue(match) 36

Slide 37

Slide 37 text

let matchers: [Matcher] = [ EqualTo(Int(42)), IdenticalTo(viewController), ] let arguments: [Any?] = storage.(...) // Missing edge cases like different argument count let match = zip(matchers, arguments) .allSatisfy { $0.matches(argument: $1) } XCTAssertTrue(match) 37

Slide 38

Slide 38 text

let matchers: [Matcher] = [ EqualTo(Int(42)), IdenticalTo(viewController), ] let arguments: [Any?] = storage.(...) // Missing edge cases like different argument count let match = zip(matchers, arguments) .allSatisfy { $0.matches(argument: $1) } XCTAssertTrue(match) 38

Slide 39

Slide 39 text

Fitting all of this together

Slide 40

Slide 40 text

protocol Mock: AnyObject { var storage: Storage { get } } 40

Slide 41

Slide 41 text

extension Mock { func recordInvocation( withIdentifier callIdentifier: String, arguments: [Any?]? ) { storage.recordInvocation( withIdentifier: callIdentifier, arguments: arguments ) } } 41

Slide 42

Slide 42 text

extension Mock { func verifyInvocation( withIdentifier callIdentifier: String, arguments: [Matcher?]? ) { let invocations = storage.filter { $0.identifier == callIdentifier } let candidate = invocations.first { match( arguments: $0.arguments, matchers: arguments ) } if candidate == nil { XCTFail() } } } 42

Slide 43

Slide 43 text

extension Mock { func verifyInvocation( withIdentifier callIdentifier: String, arguments: [Matcher?]? ) { let invocations = storage.filter { $0.identifier == callIdentifier } let candidate = invocations.first { match( arguments: $0.arguments, matchers: arguments ) } if candidate == nil { XCTFail() } } } 43

Slide 44

Slide 44 text

extension Mock { func verifyInvocation( withIdentifier callIdentifier: String, arguments: [Matcher?]? ) { let invocations = storage.filter { $0.identifier == callIdentifier } let candidate = invocations.first { match( arguments: $0.arguments, matchers: arguments ) } if candidate == nil { XCTFail() } } } 44

Slide 45

Slide 45 text

extension Mock { func verifyInvocation( withIdentifier callIdentifier: String, arguments: [Matcher?]? ) { let invocations = storage.filter { $0.identifier == callIdentifier } let candidate = invocations.first { match( arguments: $0.arguments, matchers: arguments ) } if candidate == nil { XCTFail() } } } 45

Slide 46

Slide 46 text

extension StorageMockArgumentFoo: Mock { func verifyBar(_ arg1: Matcher?, _ arg2: Matcher?) { verifyInvocation( withIdentifier: "bar", arguments: [arg1, arg2] ) } } storageMockArgumentFoo.verifyBar( EqualtTo(Int(52)), IdenticalTo(someViewController) ) 46

Slide 47

Slide 47 text

Mimus is born https://github.com/mimus-swift/Mimus 47

Slide 48

Slide 48 text

The good » Reusable matcher logic shared across different test suites » Unified framework for writing mocks 48

Slide 49

Slide 49 text

The ugly 49

Slide 50

Slide 50 text

A lot of manual work 50

Slide 51

Slide 51 text

One thing we all programmers love is... 51

Slide 52

Slide 52 text

Automation 52

Slide 53

Slide 53 text

class StorageMockArgumentFoo: Mock { let storage: Storage = Storage() func bar(arg1: Int, arg2: UIViewController) { recordInvocation( withIdentifier: "bar", arguments: [arg1, arg2] ) } func verifyBar(_ arg1: Matcher?, _ arg2: Matcher?) { verifyInvocation( withIdentifier: "bar", arguments: [arg1, arg2] ) } } 53

Slide 54

Slide 54 text

Enter Sourcery 54

Slide 55

Slide 55 text

“Sourcery is a code generator for Swift language, built on top of Apple's own SwiftSyntax. It extends the language abstractions to allow you to generate boilerplate code automatically.” Krzysztf Zablocki 55

Slide 56

Slide 56 text

// sourcery: automockable protocol ArgumentFoo { func bar(arg1: Int, arg2: UIViewController) } 56

Slide 57

Slide 57 text

class GeneratedMockArgumentFoo: ArgumentFoo, Mock { let storage: Storage = Storage() func bar(arg1: Int, arg2: UIViewController) { recordInvocation(withIdentifier: "barArg1Arg2", arguments: [arg1, arg2]) } class InstanceVerifier: Verifier { func bar(arg1: Matcher?, arg2: Matcher?) { mock.verifyInvocation( withIdentifier: "barArg1Arg2", arguments: [arg1, arg2] ) } } func verify() -> InstanceVerifier { return InstanceVerifier(mock: self) } } 57

Slide 58

Slide 58 text

class GeneratedMockArgumentFoo: ArgumentFoo, Mock { let storage: Storage = Storage() func bar(arg1: Int, arg2: UIViewController) { recordInvocation(withIdentifier: "barArg1Arg2", arguments: [arg1, arg2]) } class InstanceVerifier: Verifier { func bar(arg1: Matcher?, arg2: Matcher?) { mock.verifyInvocation( withIdentifier: "barArg1Arg2", arguments: [arg1, arg2] ) } } func verify() -> InstanceVerifier { return InstanceVerifier(mock: self) } } 58

Slide 59

Slide 59 text

class GeneratedMockArgumentFoo: ArgumentFoo, Mock { let storage: Storage = Storage() func bar(arg1: Int, arg2: UIViewController) { recordInvocation(withIdentifier: "barArg1Arg2", arguments: [arg1, arg2]) } class InstanceVerifier: Verifier { func bar(arg1: Matcher?, arg2: Matcher?) { mock.verifyInvocation( withIdentifier: "barArg1Arg2", arguments: [arg1, arg2] ) } } func verify() -> InstanceVerifier { return InstanceVerifier(mock: self) } } 59

Slide 60

Slide 60 text

let mock = GeneratedMockArgumentFoo() (...) mock.verify().bar( arg1: EqualTo(Int(10)), arg2: IdenticalTo(someViewController) ) 60

Slide 61

Slide 61 text

Mimus + Sourcery = ❤ 61

Slide 62

Slide 62 text

func verify( mode: VerificationMode = .times(1), file: StaticString = #file, line: UInt = #line ) -> InstanceVerifier { return InstanceVerifier(mock: self, mode: mode, file: file, line: line) } public enum VerificationMode { case never case atLeast(Int) case atMost(Int) case times(Int) } 62

Slide 63

Slide 63 text

let mock = GeneratedMockArgumentFoo() (...) mock.verify(mode: .atLeast(1)).bar( arg1: AnyMatcher(), arg2: IdenticalTo(viewController) ) 63

Slide 64

Slide 64 text

Mimus Stencil also offers » Functions with return values1 » Throwing functions1 » Vars 1 Currently fairly basic implementation, more advanced usage is in the works 64

Slide 65

Slide 65 text

That's all folks! 65

Slide 66

Slide 66 text

Other Notable Frameworks » SwiftMockGeneratorForXcode » Parrot » SwiftMock » SwiftyMocky » Cuckoo 66

Slide 67

Slide 67 text

Useful links » https://github.com/paweldudek/good-tdd-stuff » https://github.com/mimus-swift/Mimus » https://speakerdeck.com/paweldudek/swift-mocks 67