Dobby

Swift helpers for mocking and stubbing

165
14
Swift

Dobby

Dobby provides a few helpers for mocking and stubbing.

Matchers

Matchers can be matched with values, serving as the fundamental building block for mocking and stubbing. There are many functions that help creating matchers for (equatable) types, including optionals, tuples, arrays, and dictionaries with equatable elements:

matches { $0 == value } // matches value
any() // matches anything
not(0) // matches anything but 0
none() // matches Optional<T>.None (nil)
some(1) // matches Optional<T>.Some(1)
equals(1) // matches 1
equals((1, 2)) // matches (1, 2)
equals((1, 2, 3)) // matches (1, 2, 3)
equals((1, 2, 3, 4)) // matches (1, 2, 3, 4)
equals((1, 2, 3, 4, 5)) // matches (1, 2, 3, 4, 5)
equals([1, 2, 3]) // matches [1, 2, 3]
equals([1: 1, 2: 2, 3: 3]) // matches [1: 1, 2: 2, 3: 3]

Matchers may also be nested:

matches((matches { $0 == 0 }, any(), 2)) // matches (0, _, 2)
matches((not(equals(3)), some(any()))) // matches (not(3), _)
matches([any(), equals(4)]) // matches [_, 4]
matches(["key": matches { $0 == 5 }]) // matches ["key": 5]

Mocks

Mocks can be used to verify that all set up expectations have been fulfilled.

Strict mocks

By default, mocks are strict and the order of expectations matters, meaning all interactions must be expected and occur in the order they were expected:

let mock = Mock<[Int]>()
mock.expect(matches([any(), matches { $0 > 0 }])) // expects [_, n > 0]
mock.record([0, 1]) // succeeds
mock.verify() // succeeds
mock.record([1, 0]) // unexpected, fails (fast)

The order of expectations may also be ignored:

let mock = Mock<[Int]>(ordered: false)
mock.expect(matches([0, 1]))
mock.expect(matches([1, 0]))
mock.record([1, 0]) // succeeds
mock.record([0, 1]) // succeeds
mock.verify() // succeeds
mock.record([0, 0]) // unexpected, fails (fast)

Nice mocks

Nice mocks allow unexpected interactions while still respecting the order of expectations:

let mock = Mock<[Int?]>(strict: false)
mock.expect(matches([some(0)]))
mock.expect(matches([some(any())]))
mock.record([nil]) // unexpected, ignored
mock.record([1]) // out of order, ignored
mock.record([0]) // succeeds
mock.verify() // fails
mock.record([1]) // succeeds
mock.verify() // succeeds

Of course, nice mocks can ignore the order of expectations too:

let mock = Mock<[String: Int?]>(strict: false, ordered: false)
mock.expect(matches(["zero": some(0)]))
mock.expect(matches(["none": none()]))
mock.record(["some": 1]) // unexpected, ignored
mock.record(["none": nil]) // succeeds
mock.record(["zero": 0]) // succeeds
mock.verify() // succeeds

Negative expectations

In addition to normal expectations, nice mocks allow negative expectations to be set up:

let mock = Mock<Int>(nice: true)
mock.reject(0)
mock.record(0) // rejected, fails (fast)

Verification with delay

Verification may also be performed with a delay, allowing expectations to be fulfilled asynchronously:

let mock = Mock<Int>()
mock.expect(1)

let mainQueue: DispatchQueue = .main
mainQueue.asyncAfter(deadline: .now() + 1) {
    mock.record(1) // succeeds
}

mock.verify(delay: 2) // succeeds

Stubs

Stubs, when invoked, return a value based on their set up behavior, or, if an interaction is unexpected, throw an error. Behavior is matched in order, i.e., the function or return value associated with the first matcher that matches an interaction is invoked/returned:

let stub = Stub<(Int, Int), Int>()
let behavior = stub.on(equals((2, 3)), return: 4)
stub.on(matches((any(), any()))) { x, y in x * y }
try! stub.invoke((2, 3)) // returns 4
try! stub.invoke((3, 3)) // returns 9

Behavior may also be disposed:

behavior.dispose()
try! stub.invoke((2, 3)) // returns 6

Example

The helpers provided for mocking and stubbing can be used with any testing approach, including protocol test implementations, test subclasses, etc. For example, imagine you want to verify interactions with the following class and change its behavior:

class MyClass {
  func myMethod(fst: String, _ snd: String) -> String {
    return fst + snd
  }
}

Writing a test subclass for the given class is very simple:

class MyClassMock: MyClass {
  let myMethodMock = Mock<(String, String)>()
  let myMethodStub = Stub<(String, String), String>()
  override func myMethod(fst: String, _ snd: String) -> String {
    myMethodMock.record((fst, snd))
    // Throw an exception if the stub doesn't define any behavior for the interaction.
    return try! myMethodStub.invoke((fst, snd))
  }
}

The test subclass allows you to verify that all your set up expectations are fulfilled and enables you to change its behavior on-the-fly:

let myClassMock = MyClassMock()
myClassMock.myMethodMock.expect(matches(("Hello", "World")))
myClassMock.myMethodStub.on(any()) { fst, snd in fst }
myClassMock.myMethod("Hello", "World") // returns "Hello"
myClassMock.myMethodMock.verify() // succeeds

If you ever find yourself wanting to use a mock or stub with several interactions of different types, consider using an equatable enum to define these interactions.

Documentation

Please check out the source and tests for further documentation.

About

Dobby was born at trivago 🏭