MockNStub 7.0.0

MockNStub 7.0.0

Maintained by Menno Lovink.



 
Depends on:
InjectableLoggers~> 2
ArrayPlusTuple~> 2
 

MockNStub 7.0.0

  • By
  • mennolovink

Mock 'N Stub

Version Platform License

Code completed Mocking and Stubbing for Swift protocols and classes.

Example

To see the example project, run the following in your terminal:

pod try MockNStub

Setup

Just add:

import MockNStub

to the files where you need to create mocks or stubs.

All Mocks are Stubs

All created mocks conform to the Mocking protocol and since Mocking conforms to the Stubbing protocol, all created mocks can automatically be used as stubs too.

Wenever you feel that an explicit stub needs to support Mocking, all you need to do is change it's conformance from Stubbing to Mocking.

Class and Protocol Mocks/Stubs share the exact same interface

The implementations in MockNStub are completely protocol oriented. This allows the interface of class and protocol mocks (and stubs) to be exactly the same. All explicit stubs conform to Stubbing and all mocks conform to Mocking. There's never a need to inherit from a concrete type from this library.

Stubbing

Creating stubs

Using function names

class UITableViewDataSourceStub: Stubbing, UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return didCallFunction(withArguments: tableView, section)
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return didCallFunction(withArguments: tableView, indexPath)
    }
}

Notes:

  • No need to manually provide a function name.

Adding return values to stubs

Return values can be added as many times as desired, in the case where they are provided for the same signature, the value that was last provided is returned.

Using function names

Considering:

let stub = UITableViewDataSourceStub()

You can add stub values like this:

stub.given("tableView(_:numberOfRowsInSection:)", willReturn: 0)
stub.given("tableView(_:cellForRowAt:)", willReturn: UITableViewCell())

Or when needing to be more specific, like this:

stub.given("tableView(_:numberOfRowsInSection:)"), withArgumentsThatMatch: ArgumentMatcher(matcher: { (args: (UITableView, Int)) -> Bool in
	return args.0 === expectedTableView && args.1 == 2
}), willReturn: 42)

Notes:

  • Argument matcher won't match if argument types are not correct.

Mocking

Creating mocks

Using function names

class UITableViewDataSourceMock: NSObject, Mocking, UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return didCallFunction(withArguments: tableView, section)
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return didCallFunction(withArguments: tableView, indexPath)
    }
}

Creating expectations

Using function names

let mock = UITableViewDataSourceMock()

You can add expectations like this:

mock.expect(callToFunction: "tableView(_:cellForRowAt:)")
mock.expect(callToFunction: "tableView(_:numberOfRowsInSection:)")

Or when needing to be more specific, like this:

mock.expect(callToFunction: "tableView(_:numberOfRowsInSection:)", withArgumentsThatMatch: ArgumentMatcher(matcher: { (args: (UITableView, Int)) -> Bool in
	return args.0 === tableView && args.1 == 42                        
}))

It's also possible to expect an exact amount of calls:

mock.expect(.exactly(amount: 42), callsToFunction: "tableView(_:cellForRowAt:)")

or

mock.expect(.exactly(amount: 42), callsToFunction: "tableView(_:numberOfRowsInSection:)", withArgumentsThatMatch: ArgumentMatcher(matcher: { (args: (UITableView, Int)) -> Bool in
	return args.0 === tableView && args.1 == 42                        
}))

Verifying

regardless of how methods have been identified:

mock.verify()

Notes:

  • This will result in an XCT failure when one ore more expectations have not been met.

Properties

Mocking and stubbing properties is done like expected.

Using function names

var title: String {
    get {
        return didCallFunction()
    }
    set {
        didCallFunction(withArguments: newValue)
    }
}

Notes:

  • This get set pattern is identical on any property.

Default return values of didCall()

Within the Mocking and Stubbing protocols there's a quite a bunch of implementations for the didCall methods. Because of Swifts support for type inference, the correct method will be used at compile time. For instance when return didCallFunction() a non void implementation of didCallFunction() will be used. Even more exciting, when return didCallFunction() is called in a method that returns a value that conforms to ProvidingDefaultStubValue there will be no need to unwrap the result of didCallFunction because the default value is known (and provided) through the default protocol implementation. Note: these default stub values will only be provided when no other values are provided through the given... methods.

Don't worry too much about what is explained above, long story short: your IDE will always give you the most sensible option that's available.

In the case where a type that does not conform to ProvidingDefaultStubValue needs to be returned. The compiler won't sugest (and allow) a version of didCall.. that returns a nonoptional value. You can do three things in this case:

  • Make that type conform to ProvidingDefaultStubValue
    • If you do this for a type from one of Apple's libraries, a pull request to this repo containing this extension would be highly appreciated.
  • Manually provide a default value in case nil is provided: return didCallFunction() ?? MyType()
  • Force unwrap the return value provided by the didCall
    • In this case you do want to make sure a value is present using the given.. methods.
      • When this isn't done, your test will crash when calling the stubbed or mocked method.
      • However, every force unwrap of a nil value is known before it occurs and will cause detailed diagnosics to be logged to the console which shows what was expected to happen vs. what actually happened:Screenshot Missing

Types that currently conform to ProvidingDefaultStubValue

  • Most types from the Swift Standard library
  • Most commonly used types from UIKit
  • Most commonly used types CoreGraphics
  • All types that inherit from NSObject
    • Dislaimer; these subclasses do need to adhere to the Liskov Substitution Principle or in simpler terms: don't have a fatalError() or anything similar in their init()

Here's an overview of all types that currently conform to ProvidingDefaultStubValue

Defining function ID's

A way of reducing errors caused by typo's is by having your Mocks and Stubs conform to DefiningFunctionID

Conforming to DefiningFunctionID is done as follows:

extension UITableViewStub : DefiningFunctionID {
    typealias FunctionID = FuncID
    
    enum FuncID: String {
        case numberOfRows
        case cellForRowAt
    }
}

Note: the example above is done for a stub but is done just the same for mocks

Conforming to DefiningFunctionID will unlock the following range of mock and stub methods:

didCallFunction(withID: .numberOfRows)
mock.expect(callToFunctionWithID: .numberOfRows)

There's more to come..

Planned features

Can be viewed in the roadmap.

Anything missing?

Create a feature request and it will likely be picked up.

Installation

MockNStub is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'MockNStub'

License

MockNStub is available under the MIT license. See the LICENSE file for more info.