Swift helpers for mocking and stubbing
Dobby provides a few helpers for mocking and stubbing.
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 can be used to verify that all set up expectations have been fulfilled.
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 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
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 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, 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
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.
Please check out the source and tests for further documentation.
Dobby was born at trivago 🏭