Swiftpack.co - Package - birdrides/mockingbird

Mockingbird

Package managers License Slack

Mockingbird is a convenient mocking framework for Swift.

// Mocking
let bird = mock(Bird.self)

// Stubbing
given(bird.getName()) ~> "Ryan"

// Verification
verify(bird.fly()).wasCalled()

Installation

Mockingbird comes in two parts, both of which should be installed:

  1. The Mockingbird Framework provides functions for mocking, stubbing, and verification in tests.
  2. The Mockingbird CLI generates mocks.

CocoaPods

Add the framework to a test target in your Podfile, making sure to include the use_frameworks! option.

target 'ATestTarget' do
  use_frameworks!
  pod 'MockingbirdFramework', '~> 0.8.0'
end

This will download and install the CLI during the next pod install.

Carthage

Add the framework to your Cartfile.

github "birdrides/mockingbird" ~> 0.8.0

And copy the Carthage script into your project root.

$ carthage update --no-build
$ cp Carthage/Checkouts/mockingbird/Scripts/carthage-update.sh ./

Use the script to checkout and build Carthage dependencies instead of carthage update.

$ ./carthage-update.sh

Then download and install the CLI.

$ cd Carthage/Checkouts/mockingbird
$ make install-prebuilt

Swift Package Manager

Add https://github.com/birdrides/mockingbird as a dependency and link it to your unit test target.

Then download and install the CLI by selecting MockingbirdCLI.pkg from Releases.

From Source

Clone the repository and build the MockingbirdFramework scheme for the desired platform. Drag the built Mockingbird.framework product into your project and link the library to your test target.

$ git clone https://github.com/birdrides/mockingbird.git
$ cd mockingbird
$ open Mockingbird.xcodeproj

Then build and install the CLI.

$ make install

Setup

Mockingbird generates mocks using the mockingbird command line tool which can be integrated into your build process in many different ways.

Automatic Integration

Use the Mockingbird CLI to set up a destination unit test target. List all source targets that should generate mocks. Below, Mockingbird will mock types in Bird and BirdManagers which can then be used in BirdTests.

$ mockingbird install \
  --targets Bird BirdManagers \
  --destination BirdTests

Manual Integration

Add a Run Script Phase to each target that should generate mocks.

mockingbird generate

By default, Mockingbird will generate target mocks into the $(SRCROOT)/MockingbirdMocks directory. You can specify a custom output location for each target using the outputs CLI option.

Once generated, you must include each .generated.swift mock file as part of your unit test target sources.

Excluding Files

You can exclude unwanted or problematic sources from being mocked by adding a .mockingbird-ignore file. Mockingbird follows the same pattern format as .gitignore and scopes ignore files to their enclosing directory.

Usage

An example demonstrating basic usage of Mockingbird can be found at TreeTests.swift.

Mocking

Mocking lets you create objects which can be passed in place of the original type. Generated mock types are always suffixed with Mock.

/* Bird.swift */
protocol Bird {
  var name: String { get set }
  func canChirp(volume: Int) -> Bool
  func fly()
}

/* Tests.swift */
let bird = mock(Bird.self)  // Returns a `BirdMock`

You can also mock classes that have designated initializers. Keep in mind that class mocks rely on subclassing which has certain limitations, so consider using protocols whenever possible.

/* BirdClass.swift */
class BirdClass {
  let name: String
  init(name: String) {
    self.name = name
  }
}

/* Tests.swift */
let birdClass = mock(BirdClass.self).initialize(name: "Ryan")

Stubbing

Stubbing allows you to define a custom value to return when a mocked method is called.

given(bird.getName()) ~> "Ryan"

You can use an argument matcher when stubbing methods with parameters. Stubs added later have precedence over those added earlier, so stubs containing specific matchers should be added last.

given(bird.canChirp(volume: any())) ~> false    // Matches any volume
given(bird.canChirp(volume: notNil())) ~> true  // Matches any non-nil volume
given(bird.canChirp(volume: 10)) ~> false       // Matches volume = 10

Stub variables with their getter and setter methods.

given(bird.getName()) ~> "Big Bird"
given(bird.setName(any())) ~> { print($0) }

Getters can be stubbed to automatically save and return values.

given(bird.getName()) ~> lastSetValue(initial: "One")
bird.name = "Two"
assert(bird.name == "Two")

It’s possible to stub multiple methods with the same return type in a single call.

given(
  birdOne.getName(),
  birdTwo.getName()
) ~> "Big Bird"

Verification

Verification lets you assert that a mock received a particular invocation during its lifetime.

/* Tree.swift */
class Tree {
  let bird: Bird
  init(with bird: Bird) {
    self.bird = bird
  }

  func shake() {
    bird.fly()
  }
}

/* Tests.swift */
let tree = Tree(with: bird)
tree.shake()  // Shaking the tree should scare the bird away
verify(bird.fly()).wasCalled()

It’s possible to verify that an invocation was called a specific number of times with a count matcher.

verify(bird.fly()).wasNeverCalled()            // n = 0
verify(bird.fly()).wasCalled(exactly(10))      // n = 10
verify(bird.fly()).wasCalled(atLeast(10))      // n ≥ 10
verify(bird.fly()).wasCalled(atMost(10))       // n ≤ 10
verify(bird.fly()).wasCalled(between(5...10))  // 5 ≤ n ≤ 10

Count matchers also support chaining and negation using logical operators.

verify(bird.fly()).wasCalled(not(exactly(10)))           // n ≠ 10
verify(bird.fly()).wasCalled(exactly(10).or(atMost(5)))  // n = 10 || n ≤ 5

Sometimes you need to perform custom checks on received parameters by using an argument captor.

let nameCaptor = ArgumentCaptor<String>()
verify(bird.setName(nameCaptor.matcher)).wasCalled()
assert(nameCaptor.value?.hasPrefix("R"))

You can test asynchronous code by using an eventually block which returns an XCTestExpectation.

DispatchQueue.main.async {
  Tree(with: bird).shake()
}
let expectation = eventually {
  verify(bird.fly()).wasCalled()
  verify(bird.chirp()).wasCalled()
}
wait(for: [expectation], timeout: 1.0)

Verifying doesn’t remove recorded invocations, so it’s safe to call verify multiple times (even if not recommended).

verify(bird.fly()).wasCalled()  // If this succeeds...
verify(bird.fly()).wasCalled()  // ...this also succeeds

For methods overloaded by return type, you should help the compiler by specifying the type returned.

/* Bird.swift */
protocol Bird {
  func getMessage<T>() -> T
  func getMessage() -> String
  func getMessage() -> StaticString
}

/* Tests.swift */
verify(bird.getMessage()).returning(String.self).wasCalled()

Resetting Mocks

Occasionally it’s necessary to remove stubs or clear recorded invocations.

reset(bird)                 // Removes all stubs and recorded invocations
clearStubs(on: bird)        // Only removes stubs
clearInvocations(on: bird)  // Only removes recorded invocations

Argument Matching

Argument matchers allow wildcard matching of arguments during stubbing or verification.

any()                    // Matches any value
any(of: 1, 2, 3)         // Matches any value in {1, 2, 3}
any(where: { $0 > 42 })  // Matches any number greater than 42
notNil()                 // Matches any non-nil value

For methods overloaded by parameter type (such as with generics), using a matcher may cause ambiguity for the compiler. You can help the compiler by specifying an explicit type in the matcher.

any(Int.self)
any(Int.self, of: 1, 2, 3)
any(Int.self, where: { $0 > 42 })
notNil(String?.self)

You can also match elements or keys within collection types.

any(containing: 1, 2, 3)  // Matches any collection with values {1, 2, 3}
any(keys: "a", "b", "c")  // Matches any dictionary with keys {"a", "b", "c"}
any(count: atMost(42))    // Matches any collection with at most 42 elements
notEmpty()                // Matches any non-empty collection

If you provide a concrete instance of an Equatable type, argument values will be compared using equality. Types that don’t conform to Equatable will be compared by reference.

// Many Swift stdlib types such as `String` conform to `Equatable`
verify(bird.setName("Ryan")).wasCalled()

Supporting Source Files

Add supporting source files to mock inherited types defined outside of your project. You should always provide supporting source files when working with system frameworks like UIKit or precompiled external dependencies.

/* MyEquatableProtocol.swift */
protocol MyEquatableProtocol: Equatable {
  // ...
}

/* MockingbirdSupport/Swift/Equatable.swift */
public protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}

Setup

Mockingbird includes supporting source files for Foundation, UIKit, and other common system frameworks. For automatic integration, simply copy the MockingbirdSupport folder into your project’s source root.

If you share supporting source files between projects, you can specify a custom --support directory when running the CLI installer or generator.

Structure

Supporting source files should be contained in a directory that matches the module name. You can define submodules and transitive dependencies by nesting directories.

MockingbirdSupport/
├── Foundation/
│   └── ObjectiveC/
│       └── NSObject.swift
└── Swift/
    └── Codable.swift
    └── Comparable.swift
    └── Equatable.swift
    └── Hashable.swift

With the above file structure, NSObject can be imported from both the Foundation and ObjectiveC modules.

Performance

Mockingbird was built to be fast. Its current baseline is under 1 ms per generated mock. See Performance for benchmarks and methodology.

Mockingbird CLI

Generate

Generate mocks for a set of targets in a project.

mockingbird generate

| Option | Default Value | Description | | --- | --- | --- | | --project | (inferred) | Path to your project’s .xcodeproj file. | | --targets | $TARGET_NAME | List of target names to generate mocks for. | | --srcroot | $SRCROOT | The folder containing your project’s source files. | | --outputs | (inferred) | List of mock output file paths for each target. | | --support | (inferred) | The folder containing supporting source files. | | --condition | (none) | Compilation condition to wrap all generated mocks in, e.g. DEBUG. |

| Flag | Description | | --- | --- | | --disable-module-import | Omit @testable import <module> from generated mocks. | | --only-protocols | Only generate mocks for protocols. | | --disable-swiftlint | Disable all SwiftLint rules in generated mocks. | | --disable-cache | Ignore cached mock information stored on disk. |

Install

Set up a destination (unit test) target.

mockingbird install

| Option | Default Value | Description | | --- | --- | --- | | --targets | (required) | List of target names that should generate mocks. | | --destination | (required) | The target name where the Run Script Phase will be installed. | | --project | (inferred) | Your project’s .xcodeproj file. | | --srcroot | <project>/../ | The folder containing your project’s source files. | | --outputs | (inferred) | List of mock output file paths for each target. | | --support | (inferred) | The folder containing supporting source files. | | --condition | (none) | Compilation condition to wrap all generated mocks in, e.g. DEBUG. | | --loglevel | (none) | The log level to use when generating mocks, quiet or verbose |

| Flag | Description | | --- | --- | | --ignore-existing | Don’t overwrite existing Run Scripts created by Mockingbird CLI. | | --asynchronous | Generate mocks asynchronously in the background when building. | | --only-protocols | Only generate mocks for protocols. | | --disable-swiftlint | Disable all SwiftLint rules in generated mocks. | | --disable-cache | Ignore cached mock information stored on disk. |

Uninstall

Remove Mockingbird from a (unit test) target.

mockingbird uninstall

| Option | Default Value | Description | | --- | --- | --- | | --targets | (required) | List of target names to uninstall the Run Script Phase. | | --project | (inferred) | Your project’s .xcodeproj file. | | --srcroot | <project>/../ | The folder containing your project’s source files. |

Global Options

| Flag | Description | | --- | --- | | --verbose | Log all errors, warnings, and debug messages. | | --quiet | Only log error messages. |

Inferred Paths

--project

Mockingbird will first check if the environment variable $PROJECT_FILE_PATH was set (usually by an Xcode build context). It will then perform a shallow search of the current working directory for an .xcodeproj file. If multiple .xcodeproj files exist then you must explicitly provide a project file path.

--outputs

By default Mockingbird will generate mocks into the $(SRCROOT)/MockingbirdMocks directory with the file name $(PRODUCT_MODULE_NAME)Mocks.generated.swift.

--support

Mockingbird will recursively look for supporting source files in the $(SRCROOT)/MockingbirdSupport directory.

Resources

Github

link
Stars: 91
Help us keep the lights on

Used By

Total: 0

Releases

0.8.0 - Nov 1, 2019

Breaking

  • None

Experimental

  • Add per-module caching of generated mocks to reduce the overhead in reading large Xcode project files Andrew Chang

Enhancements

  • Improve CLI error messaging (https://github.com/birdrides/mockingbird/pull/11) Sterling Hackley
  • Improve support for mocking classes conforming to built-in Swift protocols with implicitly required initializers such as Decodable Andrew Chang
  • Improve handling of imports and compilation directives within comment blocks and string literals Andrew Chang
  • Add loglevel option to the CLI installer to specify a logging level to use when generating mocks Andrew Chang

Bug Fixes

  • Fix fnmatch implementation on macOS Catalina (https://github.com/birdrides/mockingbird/pull/15) Andrew Chang
  • Remove generic type alphabetization (https://github.com/birdrides/mockingbird/pull/14) Andrew Chang
  • Handle non-alphanumeric target names (https://github.com/birdrides/mockingbird/pull/10) Sterling Hackley
  • Fix class-only protocol initializer generation (https://github.com/birdrides/mockingbird/pull/9) Andrew Chang
  • Fix handling of failable initializers with generic type constraints Andrew Chang

0.7.0 - Sep 23, 2019

Breaking

  • None

Experimental

  • None

Enhancements

Bug Fixes

0.6.1 - Sep 18, 2019

Patch Notes

  • Fix CocoaPods regression caused by renaming the Podspec from Mockingbird to MockingbirdFramework Andrew Chang
  • Fix handling of opaque types with designated initializers Andrew Chang
  • Fix automatic conformance of Codable / Encodable / Decodable opaque types Andrew Chang

0.6.0 - Sep 17, 2019

Notes

This is a quiet release of Mockingbird to gather initial developer feedback before the full rollout next week.

Breaking

  • Rename --src-targets to --targets in the CLI installer for simplicity Andrew Chang

Experimental

  • Flag mocked types that inherit unparsed types, such as from the Swift standard library and auto-generate conformance for common Self constrained protocols in the Swift standard library Andrew Chang

Enhancements

  • Add --verbose and --quiet logging options to CLI Andrew Chang
  • Add CI support to the repo using GitHub Actions which builds, installs, and tests the framework and CLI Andrew Chang
  • Return an exit status code of 1 when a fatal CLI error occurs Andrew Chang
  • Refactor mock generation pipeline rendering step for improved semantics and clarity Andrew Chang
  • Improve TypeFacade synchronization implementation Andrew Chang

Bug Fixes

  • Fix incorrect counting of invocations for overloaded methods Andrew Chang
  • Fix incorrect type qualification level for types which could be shadowed by types defined in external modules Andrew Chang
  • Fix mocking types that inherit types with non-public initializers defined in external modules Andrew Chang
  • Fix inherited associated type protocols and Self constrained protocols that require type qualification Andrew Chang
  • Fix conformance-based generic where clauses and type qualification of generic where clauses Andrew Chang
  • Fix support for rethrowing methods Andrew Chang
  • Fix mocking empty types with conformance or inheritance Andrew Chang
  • Fix incorrect de-duplication of inherited members in class mocks Andrew Chang

0.5.0 - Sep 14, 2019

Breaking

  • None

Experimental

  • None

Enhancements

  • Add support for using a .mockingbird-ignore file to exclude sources or source directories from being mocked Andrew Chang
  • Make mockingbird install fully set up a unit test target by accepting explicit source and destination targets, --src-targets and --destination respectively Andrew Chang
  • Support os.log signposts for improved instrumentation when using Instruments.app Andrew Chang
  • Improve CLI error handling for malformed or missing arguments Andrew Chang
  • Allow binary pinning by not removing prebuilt binary when running make install-prebuilt Andrew Chang
  • Improve performance when writing generated mock files with many mocked types Andrew Chang
  • Improve performance of running type specialization on types that don’t require specialization Andrew Chang
  • Add GitHub report issue template Andrew Chang
  • Add instructions on how to link Mockingbird to a unit test target when building from source Andrew Chang

Bug Fixes

  • Fix parsing non-open declarations in external modules Andrew Chang