Skip to content

lvsti/MockSix

Repository files navigation

MockSix

Carthage compatible Swift Package Manager compatible CocoaPods compatible Swift 4.2 platforms

MockSix is a microframework to make object mocking in Swift somewhat easier. MockSix is built upon Daniel Burbank's MockFive.

If you are using Quick+Nimble, make sure you check out the Nimble matcher extensions at NimbleMockSix as well.

Elevator pitch

MockSix simplifies manual object mocking by taking over some of the boilerplate and offering an API that is hard to use incorrectly.

Sample code:

// original interface
protocol MyClassProtocol {
   func myFunc(_ string: String) -> [Int]
}

// actual implementation
class MyClass: MyClassProtocol {
    func myFunc(_ string: String) -> [Int] {
        // ... whatever ...
        return result
    }
}

// mock implementation
class MockMyClass: MyClassProtocol, Mock {
    enum Methods: Int {
        case myFunc
    }    
    typealias MockMethod = Methods
    
    func myFunc(_ string: String) -> [Int] {
        return registerInvocation(for: .myFunc, 
                                  args: string, 
                                  andReturn: [])
    }
    
    init() {}
}

// in the test case
let mock = MockMyClass()
mock.myFunc("foobar") == []     // true
mock.stub(.myFunc, andReturn: [42])
mock.myFunc("foobar") == [42]   // true
mock.stub(.myFunc) { $0.isEmpty ? [] : [42] }
mock.myFunc("foobar") == [42]   // true

Requirements

To build: Swift 4.2
To use: macOS 10.10+, iOS 8.4+, tvOS 9.2+, Linux

Installation

Via Cocoapods: add the following line to your Podfile:

pod 'MockSix'

Via Carthage: add the following line to your Cartfile (or Cartfile.private):

github "lvsti/MockSix"

Via the Swift Package Manager: add it to the dependencies in your Package.swift:

// swift-tools-version:4.0
let package = Package(
    name: "MyAwesomeApp",
    dependencies: [
        .package(url: "https://github.com/lvsti/MockSix", from: "0.1.7"),
        // ... other dependencies ...
    ],
    targets: [
        .target(name: "MyAwesomeApp", dependencies: []),
        .testTarget(
            name: "MyAwesomeAppTests",
            dependencies: ["MyAwesomeApp", "MockSix"]
        )
    ],
)

Or just add MockSix.swift and MockSixInternal.swift to your test target.

Usage

Creating the mock implementation
  1. Conform to Mock besides the actual protocol you are creating the mock for:

    class MockFoobar: FoobarProtocol, Mock {
  2. Declare an enum for the methods ("method ID") you want to make available in the mock and set it for the MockMethod typealias:

        enum Methods: Int {
            case doThis
            case doThat
        }    
        typealias MockMethod = Methods

    The enum must have a RawValue of Int.

  3. Implement the methods by calling through registerInvocation or registerThrowingInvocation:

        func doThis(_ string: String, _ number: Int) -> [Int] {
            return registerInvocation(for: .doThis, 
                                      args: string, number, 
                                      andReturn: [])
        }
        func doThat() throws -> Double {
            return registerThrowingInvocation(for: .doThat, 
                                              andReturn: 0.0)
        }
  4. Define any properties mandated by the protocol:

        var stuff: Int = 0
    }
Using the mock
  • call resetMockSix() at the beginning of each test (typically in a beforeEach block)

  • instantiate and inject as usual:

    let foobar = MockFoobar()
    let sut = MyClass(foobar: foobar)
  • stub methods by referring to their method ID:

    // return value override
    foobar.stub(.doThis, andReturn: [42])
    
    // replace implementation with closure
    foobar.stub(.doThis) { (args: [Any?]) in
        let num = args[1]! as! Int
        return [num]
    }
    foobar.stub(.doThat) { _ in
        if arc4random() % 2 == 1 { throw FoobarError.unknown }
        return 3.14
    }
    
    // invocation count aware stubbing
    foobar.stub(.doThis, andReturn: [42], times: 1, afterwardsReturn: [43])

    CAVEAT: the return value type must exactly match that of the function, e.g. to return a conforming SomeClass instance from a function with SomeClassProtocol return type, use explicit casting:

    foobar.stub(.whatever, andReturn: SomeClass() as SomeClassProtocol)
  • remove stubs to restore the behavior defined in the mock implementation:

    foobar.unstub(.doThat)
  • access raw invocation logs (if you really need to; otherwise you are better off with the Nimble matchers):

    // the mock has not been accessed
    foobar.invocations.isEmpty
    
    // doThis(_:_:) has been called twice
    foobar.invocations
        .filter { $0.methodID == MockFoobar.Methods.doThis.rawValue }
        .count == 2
    
    // doThis(_:_:) has been called with ("42", 42)
    !foobar.invocations
        .filter { 
            $0.methodID == MockFoobar.Methods.doThis.rawValue &&
            $0.args[0]! as! String == "42" &&
            $0.args[1]! as! Int == 42
        }
        .isEmpty

Other stuff

I also wrote two blogposts about MockSix which may help you get started:

Troubleshooting

  • Invocation logs are showing unrelated calls => try calling resetMockSix() in the setup phase of each test case
  • Test crashes with cast error => make sure the types of the returned values match the return type of the stubbed function; use explicit casting where required

License

MockSix is released under the MIT license.