This repository contains helpers that make mocking, stubbing and spying in Swift much easier
Imagine you have some protocol that describes some behaviour
protocol RoboKitten {
func batteryStatus() -> Int
func jump(x x: Int, y: Int) -> Int
func canJumpAt(x x: Int, y: Int) -> Bool
func rest(completed: Bool -> () )
}
And this protocol is used somewhere
class RoboKittenController {
let kitten: RoboKitten
init(kitten: RoboKitten) {
self.kitten = kitten
}
func jumpAt(x x: Int, y: Int) -> Result {
if kitten.canJumpAt(x: x, y: y) {
kitten.jump(x: x, y: y)
return .SUCCESS
}
return .FAILURE
}
...
}
So now you want to test how protocol is used as a dependency
And you create to create mock implementation of protocol
And create fake calls for each method you want to test
So here how it will look like in SwiftyMock
class RoboKittenMock: RoboKitten {
let batteryStatusCall = FunctionCall<(), Int>()
func batteryStatus() -> Int {
return stubCall(batteryStatusCall, argument:())
}
let jumpCall = FunctionCall<(x: Int, y: Int), Int>()
func jump(x x: Int, y: Int) -> Int {
return stubCall(jumpCall, argument: (x: x, y: y))
}
let canJumpAtCall = FunctionCall<(x: Int, y: Int), Bool>()
func canJumpAt(x x: Int, y: Int) -> Bool {
return stubCall(canJumpAtCall, argument: (x: x, y: y))
}
let restCall = FunctionCall<Bool -> (), ()>()
func rest(completed: Bool -> ()) {
return stubCall(restCall, argument: completed, defaultValue: ())
}
}
If setup was correct it's really easy to specify mock behaviour by using subbed methods
Let's say we want to our mock to return some values
It's really easy
// like this
kittenMock.canJumpAtCall.returns(false)
// or like this
kittenMock.jumpCall.returns(20)
Sometimes, you have bit more complex rules when and what to return
// You can add as many filters you like
// More specific rules overrides general rules
kittenMock.canJumpAtCall
.on { $0.x < 0 }.returns(false)
.on { $0.y < 0 }.returns(false)
.returns(true) // in all other cases
And sometimes you need even more complex mock behaviour, when you need for example to pass closures into mock
protocol RoboKitten {
func rest(completed: Bool -> () )
}
kittenMock.restCall.performs { completion in
print("Mock method was called! Remove this in prod version:))")
completion(true)
}
Also from time to time, you need to be sure that method was called
beforeEach {
// Since canjump method need to return somtehing we need to specify return value
kittenMock.canJumpAtCall.returns(false)
}
it("should ask kitten if it's available to jump there") {
sut.jumpAt(x: 10, y: 20)
expect(kittenMock.canJumpAtCall.called).to(beTruthy())
}
Or you need to check that method was called exact number of times
it("should actually ask kitten to jump only once per call") {
sut.jumpAt(x: 18, y: 23)
expect(kittenMock.jumpCall.callsCount).to(equal(1))
sut.jumpAt(x: 80, y: 15)
expect(kittenMock.jumpCall.callsCount).to(equal(2))
}
All method calls are stored in the mock, so you can easily check if mock was called with correct parameters
it("should ask kitten if it's available to jump there with the same coords") {
sut.jumpAt(x: 10, y: 20)
expect(kittenMock.canJumpAtCall.capturedArgument?.x).to(equal(10))
expect(kittenMock.canJumpAtCall.capturedArgument?.y).to(equal(20))
}
ReactiveSwift stubs support
API for methods that return SignalProducer
or Action
is pretty much the same as for usual methods stubs.
Imagine RoboKitten protocol is a bit changed and returns SignalProducer instead of plain value, so we can keep and eye on battery status level.
protocol RoboKitten {
func batteryStatus() -> SignalProducer<Int, NoError>
}
Now you create mock implementation of this protocol, but instead of FunctionCall
, you're using ReactiveCall
.
The only difference is that we've added third type constraint to specify Error
.
class RoboKittenMock: RoboKitten {
let batteryStatusCall = ReactiveCall<(), Int, NoError>()
func batteryStatus() -> SignalProducer<Int, NoError> {
return stubCall(batteryStatusCall, argument:())
}
}
Since SignalProducer
is constrained by Value and Error types, we need to allow user to choose, which one to stub call with.
Luckily there's handy Result type and its implementation comes along with ReactiveSwift.
Thus in order to stub ReactiveCall
, you use Result
instead of plain value as you did with FunctionCall
.
// like this
kittenMock.batteryStatusCall.returns(.success(42))
// or
kittenMock.batteryStatusCall.returns(Result(value: 42))
// or in case you want this stub to return failure
kittenMock.batteryStatusCall.returns(.failure(ImagineThisIsError))
// or
kittenMock.batteryStatusCall.returns(Result(error: ImagineThisIsError))
Everything else stays the same :)
SwiftyMock doesn't have its own matchers, so you can use whatever matchers suits better for you :)
You can generate mocks automatically with Sourcery.
First, create sourcery config yml and specify paths to sources, templates, generated output and testable import framework for tests.
You can take a look at how .sourcery.yml
here in the root looks like.
sources:
- ./Example/SwiftyMock/RoboKitten
templates:
- ./SwiftyMock/Templates
output:
path: ./Example/RoboKittenTests/Mocks/Generated
args:
testable: SwiftyMock_Example # here you specify your application module name, that you're importing for testing
Second, annotate protocols that you want to generate mocks for, with // sourcery: Mock
comment:
// sourcery: Mock
protocol RoboKitten {
// ...
}
Third, run sourcery command sourcery --config .sourcery.yml --watch
if you want to run service that will regenerate mocks every time your source files or templates change.
Or sourcery --config .sourcery.yml
if you want to generate mocks once.