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. Building
    a robust Swift
    mocking Framework
    Pawel Dudek

    View Slide

  2. 2

    View Slide

  3. So you wanted to
    mock types
    in Swift...

    View Slide

  4. The Why

    View Slide

  5. Seams

    View Slide

  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 6

    View Slide

  7. 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

    View Slide

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

    View Slide

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

    View Slide

  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 10

    View Slide

  11. 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

    View Slide

  12. The Journey
    through
    Land of Swift Mocks

    View Slide

  13. Hand
    written
    mocks

    View Slide

  14. protocol Foo {
    func bar()
    }
    14

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. Function + Arguments
    =
    Invocation

    View Slide

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

    View Slide

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

    View Slide

  26. 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

    View Slide

  27. Matching
    arguments

    View Slide

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

    View Slide

  29. Any?

    View Slide

  30. Ability to
    match values
    against a varienty of
    different rules

    View Slide

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

    View Slide

  32. 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

    View Slide

  33. 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

    View Slide

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

    View Slide

  35. Usage
    35

    View Slide

  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)
    36

    View Slide

  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)
    37

    View Slide

  38. 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

    View Slide

  39. Fitting all of this
    together

    View Slide

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

    View Slide

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

    View Slide

  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() }
    }
    }
    42

    View Slide

  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() }
    }
    }
    43

    View Slide

  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() }
    }
    }
    44

    View Slide

  45. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  49. The ugly
    49

    View Slide

  50. A lot of
    manual
    work
    50

    View Slide

  51. One thing we all
    programmers
    love is...
    51

    View Slide

  52. Automation
    52

    View Slide

  53. 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

    View Slide

  54. Enter
    Sourcery
    54

    View Slide

  55. “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

    View Slide

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

    View Slide

  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)
    }
    }
    57

    View Slide

  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)
    }
    }
    58

    View Slide

  59. 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

    View Slide

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

    View Slide

  61. Mimus
    +
    Sourcery
    =

    61

    View Slide

  62. 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

    View Slide

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

    View Slide

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

    View Slide

  65. That's all
    folks!
    65

    View Slide

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

    View Slide

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

    View Slide