Share
Embed
Swift Mocks
Slides for plSwift presentation about writing a mocking framework for Swift.
Pawel Dudek
May 25, 2022
Transcript
67 slides
-
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 -
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 -
protocol BundleIdProvider { var bundleIdentifier: String? { get } }
extension Bundle: BundleIdProvider {} 9 -
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 -
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 -
class MockFoo: Foo { var barCalled = false func bar()
{ barCalled = true } } XCTAssertTrue(mockFoo.barCalled) 15 -
class MockFoo: Foo { var timesBarCalled = 0 func bar()
{ timesBarCalled += 1 } } XCTAssertEqual(mockFoo.timesBarCalled, 1) 16 -
But what about arguments? protocol ArgumentFoo { func bar(arg1: Int,
arg2: UIViewController) } 17 -
class MockArgumentFoo: ArgumentFoo { var timesBarCalled = 0 var barArgments:
[(Int, UIViewController)] = [] func bar(arg1: Int, arg2: UIViewController) { timesBarCalled += 1 barArgments.append((arg1, arg2)) } } 18 -
class MockArgumentFoo: ArgumentFoo { var barArgments: [(Int, UIViewController)] = []
func bar(arg1: Int, arg2: UIViewController) { barArgments.append((arg1, arg2)) } } 19 -
class MockArgumentFoo: ArgumentFoo { var barArgments: [(Int, UIViewController)] = []
func bar(arg1: Int, arg2: UIViewController) { barArgments.append((arg1, arg2)) } } XCTAssert // ??? 20 -
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 -
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 -
class Storage { var recordedInvocations: [Invocation] = [] func recordInvocation(
withIdentifier identifier: String, arguments: [Any?]? ) { recordedInvocations.append(.init( identifier: identifier, arguments: arguments )) } } 25 -
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 -
let arguments: [Any?]? = [value1, value2] let expectedArguments: [???] =
??? // Won't compile, but we want something similar to this XCTEqual(arguments, expectedArguments) 28 -
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 -
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 -
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 -
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 -
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 -
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 -
extension Mock { func recordInvocation( withIdentifier callIdentifier: String, arguments: [Any?]?
) { storage.recordInvocation( withIdentifier: callIdentifier, arguments: arguments ) } } 41 -
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 -
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 -
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 -
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 -
extension StorageMockArgumentFoo: Mock { func verifyBar(_ arg1: Matcher?, _ arg2:
Matcher?) { verifyInvocation( withIdentifier: "bar", arguments: [arg1, arg2] ) } } storageMockArgumentFoo.verifyBar( EqualtTo(Int(52)), IdenticalTo(someViewController) ) 46 -
The good » Reusable matcher logic shared across different test
suites » Unified framework for writing mocks 48 -
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 -
“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 -
// sourcery: automockable protocol ArgumentFoo { func bar(arg1: Int, arg2:
UIViewController) } 56 -
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 -
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 -
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 -
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 -
let mock = GeneratedMockArgumentFoo() (...) mock.verify(mode: .atLeast(1)).bar( arg1: AnyMatcher(), arg2:
IdenticalTo(viewController) ) 63 -
Mimus Stencil also offers » Functions with return values1 »
Throwing functions1 » Vars 1 Currently fairly basic implementation, more advanced usage is in the works 64 -
Other Notable Frameworks » SwiftMockGeneratorForXcode » Parrot » SwiftMock »
SwiftyMocky » Cuckoo 66