/ Mocking

A tiny mocking library for Swift

A tiny mocking library for Swift

Mockery

Mockery is a Mocking library for Swift. It helps you mock functionality e.g. when you unit test or develop new functionality.

With Mockery, you can easily register return values, invoke method calls, return mocked return values and inspect function executions.

Mockery supports mocking functions with optional and non-optional return values as well as resultless ones. It supports values, structs, classes and enums and doesn't put any restrains on the code you write.

Installation

Swift Package Manager

In Xcode 11 and later, the easiest way to add Mockery to your project is to use Swift Package Manager:

https://github.com/danielsaidi/Mockery.git

CocoaPods

pod 'Mockery'

Carthage

github "danielsaidi/Mockery"

Manual installation

To add Mockery to your app without a dependency manager, clone this repository and place it somewhere on disk, then add Mockery.xcodeproj to the project and Mockery.framework as an embedded app binary and target dependency.

Demo App

This repository contains a demo app. To try it out, open and run the MockeryDemo project. The app is just a white screen that prints and alerts the result of using and inspecting some mocks.

Creating a mock

Consider that you have the following protocol:

protocol TestProtocol {
    
    func functionWithResult(arg1: String, arg2: Int) -> Int
    func functionWithOptionalResult(arg1: String, arg2: Int) -> Int?
    func functionWithoutResult(arg: String)
}

To mock TestProtocol, you just have to create a mock class that inherits Mock and implements TestProtocol:

class TestMock: Mock, TestProtocol {
    
    func functionWithResult(arg1: String, arg2: Int) -> Int {
        return invoke(functionWithResult, args: (arg1, arg2))
    }

    func functionWithOptionalResult(arg1: String, arg2: Int) -> Int? {
        return invoke(functionWithOptionalResult, args: (arg1, arg2))
    }
    
    func functionWithoutResult(arg: String) {
        invoke(functionWithoutResult, args: (arg))
    }
}

When you call these functions, the mock will record the invoked method calls and return any registered return values (or crash if you haven't registered any).

Using a mock recorder

If your mock has to inherit another class (e.g. a mocked view controller), you can use a mock recorder under the hood, like this:

class TestMock: TestClass, TestProtocol {

    var recorder = Mock()
    
    func functionWithResult(arg1: String, arg2: Int) -> Int {
        return recorder.invoke(functionWithResult, args: (arg1, arg2))
    }

    func functionWithOptionalResult(arg1: String, arg2: Int) -> Int? {
        return recorder.invoke(functionWithOptionalResult, args: (arg1, arg2))
    }
    
    func functionWithoutResult(arg: String) {
        recorder.invoke(functionWithoutResult, args: (arg))
    }
}

When you call these functions, the class will use its recorder torecord the invoked method calls and return any registered return values (or crash if...).

Invoking function calls

Each mocked function must call invoke to record the function call, together with the input arguments and possible return value. Void functions just have to call invoke. Functions with return values must call return invoke.

After calling the mocked functions, you will be able to inspect the recorded function calls. If you haven't registered a return value for a mocked function, your app will crash.

Registering return values

If a mocked function returns a value, you must register the return value before invoking it. Failing to do so will make your tests crash with a preconditionFailure.

You register return values by calling the mock's (or recorder's) registerResult(for:result:) function:

let mock = TestMock()
mock.registerResult(for: mock.functionWithIntResult) { _ in return 123 }

Since the result block takes in the same arguments as the actual function, you can return different result values depending on the input arguments. You don't have to register a return value for functions that return an optional value.

Inspecting executions

To inspect a mock, you can use executions(for:) to get information on how many times a function did receive a call, with which input arguments and what result it returned:

_ = mock.functionWithResult(arg1: "abc", arg2: 123)
_ = mock.functionWithResult(arg1: "abc", arg2: 456)

let executions = mock.executions(of: mock.functionWithIntResult)
expect(executions.count).to(equal(3))
expect(executions[0].arguments.0).to(equal("abc"))
expect(executions[0].arguments.1).to(equal(123))
expect(executions[1].arguments.0).to(equal("abc"))
expect(executions[1].arguments.1).to(equal(456))

In case you don't recognize the syntax above, the test uses Quick/Nimble.

Registering and throwing errors

There is currently no support for registering and throwing errors, which means that async functions can't (yet) register custom return values. Until this is implemented, you can use the Mock error property.

Device limitations

Mockery uses unsafe bit casts to get the memory address of mocked functions. This only works on 64-bit devices, which means that mock-based unit tests will not work on old devices or simulators like iPad 2, iPad Retina etc.

Acknowledgements

Mockery is inspired by [Stubber][Stubber], and would not have been possible without it. The entire function address approach and escape support etc. comes from Stubber, and this mock implementation comes from there as well.

However, while Stubber uses global functions (which requires you to reset the global state every now and then), Mockery moves this logic to each mock, which means that any recorded exeuctions are automatically reset when the mock is disposed. Mockery also adds some extra functionality, like support for optional and void results.

GitHub