Coder Social home page Coder Social logo

swiftmockobject's Introduction

SwiftMockObject

Protocol-based mocks for Swift unit testing

platform: iOS | macOS | tvOS license carthage: compatible

Requirements

  • iOS 8.0+
  • macOS 10.10+
  • tvOS 10.0+
  • Swift 5

Installation

Carthage

github "bluejeans/SwiftMockObject" == 0.1.0

To add the framework to a test target, go to the Build Phases for that target (instead of the General settings) and drag SwiftMockObject.framework from the Carthage/Build folder to the Link Binary With Libraries section. Then follow the rest of Carthage instructions.

Features

To determine if SwiftMockObject is the right mocking library for you...

  • Protocol-based: SwiftMockObject works best with protocols. If you would like to mock/stub classes or create spies, this will be of limited use.
  • Mocks vs. stubs: SwiftMockObject has one Mock class that supports both stubbing and verification. You only need to create one mock class regardless of whether you are stubbing or mocking.
  • Non-strict mocks: Instead of using expect/verify, SwiftMockObject lets you assert the number of times a method was called and its arguments after the fact. This means that mocks are not strict and will not fail the test if a method was called that was not previously expected or asserted.
  • Ease of use: There is some degree of boilerplate involved in defining a mock (see "Creating a mock" below). However, once this is done, using the mock should be simple, flexible, and type-safe (no need to use strings to refer to methods or arguments).

Usage

SwiftMockObject is protocol-based. If you have an object MainClass that uses DependencyClass, DependencyClass should conform to DependencyProtocol and MainClass should use DependencyProtocol instead of DependencyClass.

Creating a mock

To create a mock for DependencyProtocol, create an enum (e.g. DependencyProtocolMethods) representing the methods of the protocol, then create a mock class that subclasses MockObject<DependencyProtocolMethods> and conforms to DependencyProtocol. The mock class can then use _onMethod() in its implementation to both track method invocations and arguments for later assertion and to provide stubbing functionality.

Example

The functionality in your main target that you want to test:

protocol DependencyProtocol {
    func withOptional(arg: Int?)
    func withComplex(arg: ComplexArg)
    func withReturn() -> Bool
    func with(name: String, callback: @escaping (Int, String) -> Void)
}

struct ComplexArg {
    let key: String
    let value: Any
}

// Actual implementation of DependencyProtocol elided...

class MainClass {
    private let dependency: DependencyProtocol

    init(dependency: DependencyProtocol) {
        self.dependency = dependency
    }

    func invokeDependencyWithComplexArg(countOf array: [Any]) {
        dependency.withComplex(arg: ComplexArg(key: "count", value: array.count))
    }

    // Other methods that use DependencyProtocol elided...
}

Creating the mock in your test target:

enum DependencyProtocolMethods {
    case withOptionalArg
    case withComplexArg
    case withReturn
    case withMultipleArgsAndCallback
}

class MockDependency: MockObject<DependencyProtocolMethods>, DependencyProtocol {
    func withOptional(arg: Int?) {
        _onMethod(.withOptionalArg, args: arg)
    }

    func withComplex(arg: ComplexArg) {
        _onMethod(.withComplexArg, args: arg)
    }

    func withReturn() -> Bool {
        return _onMethod(.withReturn, defaultReturn: true)
    }

    func with(name: String, callback: @escaping (Int, String) -> Void) {
        _onMethod(.withMultipleArgsAndCallback, args: name, callback)
    }
}

Stubbing

You can get a MethodReference using mock.methodReference(.enumCase). MethodReference supports setCustomBehavior() to specify a return value or more complex arbitrary behaviors.

Example

mock.methodReference(.withReturn).setCustomBehaviorToReturn(true)

mock.methodReference(.withOptionalReturn).setCustomBehaviorToReturnNil()

mock.methodReference(.withMultipleArgsAndCallback).setCustomBehavior({ [weak self] args in
    // args is an [Any?] and will need to be cast to the expected type
    guard let name = args[0] as? String else { XCTFail("Missing arg"); return }
    self?.doSomethingWithName(name)
})

mock.methodReference(.withMultipleArgsAndCallback).setCustomBehavior({ args in
    guard let callback = args[1] as? (Int, String) -> Void else { XCTFail("Missing arg"); return }
    callback(1, "hello")
})

Asserting method calls and arguments

SwiftMockObject provides an extension of XCTestCase that adds MOAssert() methods. This allows you to assert that a particular method was called a certain number of times, assert that a particular argument was passed, and get an argument to a method if you need to do something more complex with the argument.

By default the assertions operate on the last time the method was called. If you want a previous invocation, pass a non-nil value for whichTime. Assertions are not strict; if you omit assertions the test will not fail.

The argument and time parameters start from 1 (i.e. they are not 0-indexed).

Example

// .withArgs was called 0 times
MOAssertTimesCalled(mock.methodReference(.withArg), 0)

// .withArg was called at least once
// For the most recent invocation, the first argument was equal to 42
MOAssertArgumentEquals(mock.methodReference(.withArg), 1, 42)

// .withOptionalArg was called at least once
// For the most recent invocation, the first argument was nil
MOAssertArgumentNil(mock.methodReference(.withOptionalArg), 1)

// .withArg was called at least 3 times
// For the third invocation, the first argument was equal to 42
MOAssertArgumentEquals(mock.methodReference(.withOptionalArg), 1, 3, 42)

// .withComplexArg was called at least once
// Returns the first argument of the most recent invocation
// You may want to do this if you need to do something with the argument,
// e.g. if the argument does not conform to Equatable and you need to
// do a more complex check.
let actualArg: ComplexArg = MOAssertAndGetArgument(mock.methodReference(.withComplexArg), 1)!
XCTAssertEqual(actualArg.key, "count")
XCTAssertEqual(actualArg.value as? Int, 3)

More examples

See the TestAbstractExample.swift for a full test case based on the examples above.

More examples can be found in the Example project and its unit tests in ExampleTests.

Credits

License

See LICENSE

swiftmockobject's People

Contributors

onelittlefish avatar

Forkers

onelittlefish

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.