Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Swift Mocks

Swift Mocks

Slides for plSwift presentation about writing a mocking framework for Swift.

Pawel Dudek

May 25, 2022
Tweet

More Decks by Pawel Dudek

Other Decks in Programming

Transcript

  1. 2

  2. 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
  3. 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
  4. 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
  5. 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
  6. class MockFoo: Foo { var barCalled = false func bar()

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

    { timesBarCalled += 1 } } XCTAssertEqual(mockFoo.timesBarCalled, 1) 16
  8. class MockArgumentFoo: ArgumentFoo { var timesBarCalled = 0 var barArgments:

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

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

    func bar(arg1: Int, arg2: UIViewController) { barArgments.append((arg1, arg2)) } } XCTAssert // ??? 20
  11. 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
  12. 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
  13. class Storage { var recordedInvocations: [Invocation] = [] func recordInvocation(

    withIdentifier identifier: String, arguments: [Any?]? ) { recordedInvocations.append(.init( identifier: identifier, arguments: arguments )) } } 25
  14. 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
  15. let arguments: [Any?]? = [value1, value2] let expectedArguments: [???] =

    ??? // Won't compile, but we want something similar to this XCTEqual(arguments, expectedArguments) 28
  16. class EqualTo<T: Equatable>: 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
  17. class EqualTo<T: Equatable>: 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
  18. class IdenticalTo<T: AnyObject>: Matcher { ... } class InstanceOf<T>: Matcher

    { ... } class NotMatcher: Matcher { ...} class CaptureArgumentMatcher: Matcher { ... } class AnyMatcher: Matcher {} class SimulateCallbackMatcher<T1, T2, T3>: Matcher { ... } 34
  19. 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
  20. 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
  21. 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
  22. extension Mock { func recordInvocation( withIdentifier callIdentifier: String, arguments: [Any?]?

    ) { storage.recordInvocation( withIdentifier: callIdentifier, arguments: arguments ) } } 41
  23. 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
  24. 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
  25. 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
  26. 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
  27. extension StorageMockArgumentFoo: Mock { func verifyBar(_ arg1: Matcher?, _ arg2:

    Matcher?) { verifyInvocation( withIdentifier: "bar", arguments: [arg1, arg2] ) } } storageMockArgumentFoo.verifyBar( EqualtTo(Int(52)), IdenticalTo(someViewController) ) 46
  28. The good » Reusable matcher logic shared across different test

    suites » Unified framework for writing mocks 48
  29. 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
  30. “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
  31. 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
  32. 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
  33. 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
  34. 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
  35. Mimus Stencil also offers » Functions with return values1 »

    Throwing functions1 » Vars 1 Currently fairly basic implementation, more advanced usage is in the works 64