Code Monkey home page Code Monkey logo

mockingbird's Introduction

Mockingbird - Swift Mocking Framework

Package managers License Slack

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

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

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

What is Mockingbird?

Mockingbird is a Swift mocking framework that lets you throw away your hand-written mocks and write clean, readable tests.

  • Expansive coverage of Swift language features
    • Mock classes and protocols in a single line of code
    • Support for generics, inheritance, static members, nested classes, type aliasing, etc.
  • Seamless integration with Xcode projects
    • Automatic discovery of source and dependency files
    • Handling of external types from third-party libraries
  • Convenient testing API
    • Clear stubbing and verification error messages
    • Support for asynchronous code, in order verification, default return value stubbing, etc.

Under the Hood

Mockingbird consists of two main components: the generator and the testing framework. The generator creates mocks before each test bundle compilation by implementing protocols and subclassing classes, while the testing framework hooks into the generated code and provides APIs for mocking, stubbing, and verification.

A key design consideration was performance. Mockingbird runs an optimized parser built on SwiftSyntax and SourceKit that is ~30-40x faster than existing frameworks and supports a broad range of complex Swift features like generics and type qualification.

A Simple Example

protocol Bird {
  var canFly: Bool { get }
  func fly()
}

class Tree {
  let bird: Bird
  
  init(with bird: Bird) {
    self.bird = bird
  }
  
  func shake() {
    guard bird.canFly else { return }
    bird.fly()
  }
}

func testShakingTreeCausesBirdToFly() {
  // Given a tree with a bird that can fly
  let bird = mock(Bird.self)
  let tree = Tree(with: bird)
  given(bird.getCanFly()) ~> true
  
  // When the tree is shaken
  tree.shake()
  
  // Then the bird flies away
  verify(bird.fly()).wasCalled()
}

Installation

Select your preferred dependency manager below for installation instructions and example projects.

CocoaPods

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

target 'MyAppTests' do
  use_frameworks!
  pod 'MockingbirdFramework', '~> 0.13'
end

Initialize the pod and install the CLI.

$ pod install
$ (cd Pods/MockingbirdFramework && make install-prebuilt)

Finally, download the starter supporting source files and configure a test target. This adds a build phase to the test target that generates mocks for each listed source module. For advanced usages, see the available installer options and how to set up targets manually.

$ mockingbird download starter-pack
$ mockingbird install --target MyAppTests --sources MyApp MyLibrary1 MyLibrary2

Have questions or issues?

Carthage

Add the framework to your Cartfile.

github "birdrides/mockingbird" ~> 0.13

Build the framework with Carthage, link it to your test target, and install the CLI.

$ carthage update
$ (cd Carthage/Checkouts/mockingbird && make install-prebuilt)

Finally, download the starter supporting source files and configure a test target. This adds a build phase to the test target that generates mocks for each listed source module. For advanced usages, see the available installer options and how to set up targets manually.

$ mockingbird download starter-pack
$ mockingbird install --target MyAppTests --sources MyApp MyLibrary1 MyLibrary2

Have questions or issues?

Swift Package Manager

Add the framework as a package dependency and link it to your test target.

  1. File > Swift Packages > Add Package Dependency…
  2. Enter https://github.com/birdrides/mockingbird for the repository URL and click Next
  3. Choose “Up to Next Minor” for the version and click Next
  4. Select your test target under “Add to Target” and click Finish
Click here if you are using a Package.swift manifest file instead.
let package = Package(
  name: "MyPackage",
  dependencies: [
    .package(url: "https://github.com/birdrides/mockingbird.git", .upToNextMinor(from: "0.13.0")),
  ],
  targets: [
    .testTarget(name: "MyPackageTests", dependencies: ["Mockingbird"]),
  ]
)

In your project directory, initialize the package dependency and install the CLI.

$ xcodebuild -resolvePackageDependencies
$ DERIVED_DATA=$(xcodebuild -showBuildSettings | pcregrep -o1 'OBJROOT = (/.*)/Build')
$ (cd "${DERIVED_DATA}/SourcePackages/checkouts/mockingbird" && make install-prebuilt)

Finally, download the starter supporting source files and configure a test target. This adds a build phase to the test target that generates mocks for each listed source module. For advanced usages, see the available installer options and how to set up targets manually.

$ mockingbird download starter-pack
$ mockingbird install --target MyPackageTests --sources MyPackage MyLibrary1 MyLibrary2

Have questions or issues?

Usage

  1. Mocking
  2. Stubbing
  3. Verification
  4. Argument Matching
  5. Miscellaneous

1. Mocking

Initialized mocks can be passed in place of the original type. Protocol mocks do not require explicit initialization while class mocks should be created using initialize(…).

protocol Bird {
  init(name: String)
}
class Tree {
  init(with bird: Bird) {}
}

let bird = mock(Bird.self)  // Protocol mock
let tree = mock(Tree.self).initialize(with: bird)  // Class mock

Generated mock types are suffixed with Mock and should not be coerced into their supertype.

let bird: BirdMock = mock(Bird.self)  // The concrete type is `BirdMock`
let inferredBird = mock(Bird.self)    // Type inference also works
let coerced: Bird = mock(Bird.self)   // Avoid upcasting mocks

Reset Mocks

Reset mocks and clear specific configurations during test runs.

reset(bird)                    // Reset everything
clearStubs(on: bird)           // Only remove stubs
clearDefaultValues(on: bird)   // Only remove default values
clearInvocations(on: bird)     // Only remove recorded invocations

2. Stubbing

Stubbing allows you to define custom behavior for mocks to perform.

given(bird.canChirp()) ~> true
given(bird.canChirp()) ~> { throw BirdError() }
given(bird.canChirp(volume: any())) ~> { volume in
  return volume < 42
}

Stub Methods with Parameters

Match argument values to stub methods with parameters. Stubs added later have a higher precedence, so add stubs with specific matchers last.

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

Stub Properties

Stub properties with their getter and setter methods.

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

Getters can be stubbed to automatically save and return values.

given(bird.getName()) ~> lastSetValue(initial: "")
print(bird.name)  // Prints ""
bird.name = "Ryan"
print(bird.name)  // Prints "Ryan"

Relaxed Stubs with Default Values

Mocks are strict by default, meaning that calls to unstubbed methods will trigger a test failure. Methods returning Void do not need to be stubbed in strict mode.

let bird = mock(Bird.self)
print(bird.name)  // Fails because `bird.getName()` is not stubbed
bird.fly()        // Okay because `fly()` has a `Void` return type

To return default values for unstubbed methods, use a ValueProvider with the initialized mock. Mockingbird provides preset value providers which are guaranteed to be backwards compatible, such as .standardProvider.

let bird = mock(Bird.self)
useDefaultValues(from: .standardProvider, on: bird)
print(bird.name)  // Prints ""

You can create custom value providers by registering values for types.

var valueProvider = ValueProvider()
valueProvider.register("Ryan", for: String.self)
useDefaultValues(from: valueProvider, on: bird)
print(bird.name)  // Prints "Ryan"

Values from concrete stubs always have a higher precedence than default values.

given(bird.getName()) ~> "Ryan"
print(bird.name)  // Prints "Ryan"

useDefaultValues(from: .standardProvider, on: bird)
print(bird.name)  // Prints "Ryan"

Provide wildcard instances for generic types by conforming the base type to Providable and registering the type.

extension Array: Providable {
  public static func createInstance() -> Self? {
    return Array()
  }
}

// Provide an empty array for all specialized `Array` types
valueProvider.registerType(Array<Any>.self)

Stub a Sequence of Values

Methods that return a different value each time can be stubbed with a sequence of values. The last value will be used for all subsequent invocations.

given(bird.getName()) ~> sequence(of: "Ryan", "Sterling")
print(bird.name)  // Prints "Ryan"
print(bird.name)  // Prints "Sterling"
print(bird.name)  // Prints "Sterling"

3. Verification

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

verify(bird.fly()).wasCalled()

Verifying doesn’t remove recorded invocations, so it’s safe to call verify multiple times.

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

Verify Methods with Parameters

Match argument values to verify methods with parameters.

verify(bird.canChirp(volume: any())).wasCalled()     // Called with any volume
verify(bird.canChirp(volume: notNil())).wasCalled()  // Called with any non-nil volume
verify(bird.canChirp(volume: 10)).wasCalled()        // Called with volume = 10

Verify Properties

Verify property invocations using their getter and setter methods.

verify(bird.getName()).wasCalled()
verify(bird.setName(any())).wasCalled()

Verify the Number of Invocations

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

Argument Capturing

An argument captor extracts received argument values which can be used in other parts of the test.

let bird = mock(Bird.self)
bird.name = "Ryan"

let nameCaptor = ArgumentCaptor<String>()
verify(bird.setName(nameCaptor.matcher)).wasCalled()

print(nameCaptor.value)  // Prints "Ryan"

In Order Verification

Enforce the relative order of invocations with an inOrder verification block.

// Verify that `fly` was called before `chirp`
inOrder {
  verify(bird.fly()).wasCalled()
  verify(bird.chirp()).wasCalled()
}

Pass options to inOrder verification blocks for stricter checks with additional invariants.

inOrder(with: .noInvocationsAfter) {
  verify(bird.fly()).wasCalled()
  verify(bird.chirp()).wasCalled()
}

Asynchronous Verification

Mocked methods that are invoked asynchronously can be verified 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)

Verify Methods Overloaded by Return Type

Specify the expected return type to disambiguate overloaded methods.

protocol Bird {
  func getMessage<T>() -> T    // Overloaded generically
  func getMessage() -> String  // Overloaded explicitly
  func getMessage() -> Data
}

verify(bird.getMessage()).returning(String.self).wasCalled()

4. Argument Matching

Argument matching allows you to stub or verify specific invocations of parameterized methods.

Match Exact Values

Value types that explicitly conform to Equatable work out of the box. Note that structs able to synthesize Equatable conformance must still explicitly declare conformance.

struct Fruit: Equatable {
  let size: Int
}

verify(bird.eat(Fruit(size: 42))).wasCalled()
verify(bird.setName("Ryan")).wasCalled()

Class instances can be safely compared by reference.

class Tree {
  init(with bird: Bird) {
    bird.home = self
  }
}

let tree = Tree(with: bird)
verify(bird.setHome(tree)).wasCalled()

Match Wildcard Values and Non-Equatable Types

Argument matchers allow for wildcard and custom matching of arguments that don’t conform to Equatable.

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

Match Floating Point Values

Mathematical operations on floating point numbers can cause loss of precision. Fuzzily match floating point arguments instead of using exact values to increase the robustness of tests.

around(10.0, tolerance: 0.01)

5. Miscellaneous

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.

Using Supporting Source Files

Supporting source files are used by the generator to resolve inherited types defined outside of your project. Although Mockingbird provides a preset “starter pack” for basic compatibility with common system frameworks, you will occasionally need to add your own definitions for third-party library types. Please see Supporting Source Files for more information.

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.
--diagnostics (none) List of diagnostic generator warnings to enable.
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.
--disable-relaxed-linking Only search explicitly imported modules.

Install

Configure a test target to use mocks.

mockingbird install

Option Default Value Description
--target (required) The name of a test target to configure.
--sources (required) List of target names to generate mocks for.
--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.
--diagnostics (none) List of diagnostic generator warnings to enable.
--loglevel (none) The log level to use when generating mocks, quiet or verbose
Flag Description
--preserve-existing Don’t overwrite previously installed configurations.
--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.
--disable-relaxed-linking Only search explicitly imported modules.

Uninstall

Remove Mockingbird from a 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.

Download

Download and unpack a compatible asset bundle. Bundles will never overwrite existing files on disk.

mockingbird download <asset>

Asset Description
starter-pack Starter supporting 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.

Additional Resources

Examples and Tutorials

Help and Documentation

mockingbird's People

Contributors

andrewchang-bird avatar paiv avatar ryanmeisters avatar shackley avatar alvarhansen avatar

Watchers

James Cloos avatar

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.