- Overview
- API
- Stubbing hints
- Limitations
To prepare a mock that conforms a protocol, you have to manually create a class type that contains a single lazy var for each function of a protocol, which is then called from a body of a function, e.g.:
import StubKit
protocol Database {
func addUser(name: String) -> Bool
func addAccount(givenName: String, lastName: String) -> Bool
}
class DatabaseMock: Database {
lazy var addUserAction = stub(of: addUser)
func addUser(name: String) {
return addUserAction(name)
}
lazy var addAccountAction = stub(of: addAccount)
func addAccount(givenName: String, lastName: String) -> Bool
// pass all the arguments as a tuple
return addAccountAction((givenName, lastName))
}
}
For multi-parameter function, you should call the corresponding action variable with all arguments wrapped in a tuple.
Stubs in StubKit are loose by default so mock's functions can bee freely called.
Some types, (like Void
, String
, Int
etc.) have predefined default values that stub can return without any specific declaration. You can enable automatic stubbing it for your custom types by conforming to DefaultProvidable
type:
struct CustomType: DefaultProvidable {
static var defaultValue = CustomType(...)
}
Here is a list of predefined default values provided by StubKit library:
Type | Default value |
---|---|
String |
"" |
Bool |
false |
Int |
0 |
UInt |
0 |
Double |
0.0 |
Decimal |
0.0 |
CGFloat |
0.0 |
Data |
Data() |
Array |
[] |
Dictionary |
[:] |
Set |
[] |
Optional |
nil |
Locale |
Locale(identifier: "") |
URL |
URL(fileURLWithPath: "/") |
URLComponents |
URLComponents() |
URLQueryItem |
URLQueryItem(name: "", value: nil) |
If some return type cannot conform to DefaultProvidable
(e.g. for a tuple) or your stub should return other value than default one, you can manually specify it while stubbing:
class DatabaseMock: Database {
lazy var addUserAction = stub(of: addUser, alwaysReturn: true)
func addUser(name: String) -> Bool {
return addUserAction(name)
}
}
You can customize your stub behaviour to always throw a specific error as:
class DatabaseMock: Database {
lazy var addUserThrowingAction = stub(of: addUser, alwaysThrow: DatabaseError())
func addUserThrowing(name: String) throws -> Bool {
return addUserAction(name)
}
}
Using registration (for strictStub
) and standard (nice) stub
your function will always return the same value unilt you reregistere it with a new value. Although registrating the stub again sounds like a problem solver, keep in mind that all the assigned spies would then be unattached and skip spying arguments. Therefore StupKit provides a setup feature that can dynamically configures return value of a function without reregistration.
protocol Database {
func addAccount(givenName: String, lastName: String) -> Int
}
class DatabaseMock: Database {
lazy var addAccountAction = stub(of: addAccount)
func addAccount(givenName: String, lastName: String) -> Int {
return addAccountAction(name)
}
}
/// Testcase body
// default behaviour
databaseMock.addAccount(givenName: "John", lastName: "Appleseed") // returns 0
// custom return value
setupStub(of: &databaseMock.addAccountAction, return: 1)
databaseMock.addAccount(givenName: "John", lastName: "Appleseed") // returns 1
// override return value
setupStub(of: &databaseMock.addAccountAction, return: 2)
databaseMock.addAccount(givenName: "John", lastName: "Appleseed") // returns 2
Besides standard setup that specifies return value for all calls, sequence setup can specify custom return values for a single call (returnsOnce
/throwsOnce
) or all subsequent calls (returns
/throws
):
// custom return sequence
setupStubSequence(of: &databaseMock.addAccountAction)
.returnsOnce(1) // 1
.returnsOnce(2) // 2
.returnsOnce(3) // 3
.returns(0) // 4+
databaseMock.addAccount(givenName: "John", lastName: "Appleseed1") // returns 1
databaseMock.addAccount(givenName: "John", lastName: "Appleseed2") // returns 2
databaseMock.addAccount(givenName: "John", lastName: "Appleseed3") // returns 3
databaseMock.addAccount(givenName: "John", lastName: "Appleseed4") // returns 0
databaseMock.addAccount(givenName: "John", lastName: "Appleseed5")) // returns 0
If you don't end up stub sequence with infinite return, previous setup value has a precedance
setupStubSequence(of: &databaseMock.addAccountAction).returns(-1) // 2+
setupStubSequence(of: &databaseMock.addAccountAction)
.returnsOnce(1) // 1
.returnsOnce(2) // 1
databaseMock.addAccount(givenName: "John", lastName: "Appleseed") // returns 1
databaseMock.addAccount(givenName: "John", lastName: "Appleseed") // returns 2
databaseMock.addAccount(givenName: "John", lastName: "Appleseed") // returns -1
Setup can selectively return custom values, depending on call arguments. You can predicate selecting from:
- predicate closure
- single-argument comparison (for
Equatable
argument types) - custom argument comparison - support for functions with maxium 4 arity (e.g.
whenFirst
...whenForth
)
setupStubSequence(of: &databaseMock.addUserAction)
.when("user1")
.returns(false)
setupStubSequence(of: &databaseMock.addUserAction)
.when("user2")
.returns(true)
databaseMock.addUser(name: "user1") // returns false
databaseMock.addUser(name: "user2") // returns true
setupStubSequence(of: &databaseMock.addUserAction)
.when({$0.count > 0})
.returns(true)
databaseMock.addUser(name: "") // returns false
databaseMock.addUser(name: "user2") // returns true
protocol Database {
func addAccount(givenName: String, lastName: String) -> Int
}
setupStubSequence(of: &databaseMock.addAccountAction)
.whenFirst("Tom")
.returns(1)
setupStubSequence(of: &databaseMock.addAccountAction)
.whenFirst("Derk")
.returns(10)
setupStubSequence(of: &databaseMock.addAccountAction)
.whenFirst("Tom")
.whenSecond("Derk")
.returns(100)
databaseMock.addUser(givenName: "Tom", lastName: "Kerk") // returns 1
databaseMock.addUser(givenName: "Ole", lastName: "Derk") // return 10
databaseMock.addUser(givenName: "Ole", lastName: "De") // returns 0
databaseMock.addUser(givenName: "Tom", lastName: "Derk") // returns 100 - Last setup has a predecence
StubKit's term "strict stubbing" means that body of a protocol function has to be explicitly defined (registered) before it is ever called during test scenario. Any call to a stubbed function that hasn't been registerd crashes the test. Stubbing could be a part of strict mock implementation, where test scenario has to manually specify which functions are allowed to be called during test.
protocol Database {
func addUser(name: String) -> Bool
}
class DatabaseMock: Database {
lazy var addUserAction = strictStub(of: addUser)
func addUser(name: String) -> Bool {
return addUserAction(name)
}
}
/// Testcase
// no registration
databaseMock.addUser(name: "user1") // crash 💥
// automatic registration
databaseMock.addUserAction = registerStub(alwaysReturn: true)
databaseMock.addUser(name: "user1") // OK ✅
Strict stubbing is very restrictive so it recommended to prefer standard stubbing instead.
If your protocol contains the same name of a function Ambiguous use of 'addUser'
but with different number of parameters, you can give compiler a hint which to stub by adding full signature with parameters:
protocol DatabaseProtocol {
func addUser(_ name: String) -> Bool
func addUser(_ name: String, at: Date) -> Bool
}
class DatabaseProtocolMock: DatabaseProtocol {
lazy var addUserAction = stub(of: addUser(_:))
func addUser(_ name: String) -> Bool {
return addUserAction(name)
}
lazy var addUserAtAction = stub(of: addUser(_:at:))
func addUser(_ name: String, at date: Date) -> Bool {
return addUserAtAction((name, date))
}
}
However, for overloaded functions, above approach doesn't work so you need to specify stub's return type:
protocol DatabaseProtocol {
func addUser(_ name: String) -> Bool
func addUser(_ id: Int) -> Bool
}
class DatabaseProtocolMock: DatabaseProtocol {
lazy var addUserAction:(String) -> Bool = stub(of: addUser)
func addUser(_ name: String) -> Bool {
return addUserAction(name)
}
lazy var addUserIntAction:(Int) -> Bool = stub(of: addUser)
func addUser(_ name: Int) -> Bool {
return addUserIntAction(name)
}
}
You can change the return value for your mock for a specific instance by "registering" it. Register can by done automatically or manually.
databaseMock.addUserAction = registerStub(alwaysReturn: true)
Alternatively, to specify throwing function, you can register it with alwaysThrow
parameter:
databaseMock.addUserThrowingAction = registerStub(alwaysThrow: DatabaseError())
Optionally, you may specify custom behaviour that happens when given function is called:
databaseMock.addUserAction = { _ in
return true
}
Once you have mock ready, you can track all of its calls and arguments by introducing a spy. ArgRecords<T>
is a generic class that records them. For a mock functions that contain several parameteres, T
will become a tuple with corresponding number of items.
ArgRecords
has a custom subscript function which returnsOptional<T>
. Thanks to that, subscripted value can be safely passed toXCTAssertEqual
without crashing the app when spied functions hasn't been yet called.
// addUserSpy is ArgRecords<String> as `addUser` has a single String argument
let addUserSpy = spyCalls(of: &databaseStub.addUserAction)
databaseMock.addUser(name: "user1")
let callArg = addUserSpy[0] // "user1"
let callCount = addUserSpy.count // 1
// addUserSpy is ArgRecords<String> as `addUser` has a single String argument
let addUserSpy = spyCalls(of: &databaseStub.addUserAction)
databaseMock.addUser(name: "user1")
XCTAssertEqual(addUserSpy, ["user1"])
// addAccountSpy is ArgRecords<(String, String)> as `addAccount` has two String parameters
let addAccountSpy = spyCalls(of: &databaseStub.addAccountAction)
databaseMock.addAccount(givenName: "given", lastName: "last")
let callArg = addUserSpy[0] // ("given","last")
let callGivenNameArg = addUserSpy[0].0 // "given"
let callLastNameArg = addUserSpy[0].1 // "last"
By default, a spy keeps a strong reference to all arguments passed to the spied function. If that negatively interefers with your scenario, you can leverage weakly referenced spy:
// addSomeClassSpy is ArgWeakRecords<SomeClass>
let addSomeClassSpy = spyWeaklyCalls(of: &databaseStub.addSomeClass)
databaseMock.addSomeClass(SomeClass())
let callArg = addSomeClassSpy[0] // `nil`, unless some other part keeps strong reference to `SomeClass()`
let callCount = addUserSpy.count // 1
By default, spy keeps track of all arguments passed to the function. To keep only a set them, you can transform it in transform
function, before it gets stored in a spy object.
// addAccountGivenSpy is ArgRecords<String> as only `transform` considers only the first argument
let addAccountSpy = spyCalls(of: &databaseStub.addAccountAction, transform: {$0.0})
databaseMock.addAccount(givenName: "given", lastName: "last")
let callArg = addUserSpy[0] // "given"
Setup sequences also can be used for expected arguments verification by assigning an expectation to the sequence.
let tomSequence = setupStubSequence(of: &databaseMock.addAccountAction)
.whenFirst("Tom")
.returns(1)
.expect(.once)
let derkSequence = setupStubSequence(of: &databaseMock.addAccountAction)
.whenSecond("Derk")
.expect(.once)
let tomDerkSequence = setupStubSequence(of: &databaseMock.addAccountAction)
.whenFirst("Tom")
.whenSecond("Derk")
.expect(.once)
let notVerifiedSequence = setupStubSequence(of: &databaseMock.addAccountAction)
.expect(.never)
databaseMock.addUser(givenName: "Tom", lastName: "On")
databaseMock.addUser(givenName: "John", lastName: "Derk")
SKTVerify(tomSequence) // ✅
XCTAssertTrue(tomSequence.verify()) // Equivalent to above call
SKTVerify(derkSequence) // ✅
SKTVerify(tomDerkSequence) // 🛑 - "Sequence expectation not met: Called 0 times while expected once"
💥 // notVerifiedSequence has not been verified
Keep in mind that all the expected sequences have to be verified by
SKTVerify
orsequence.verify()
. Deallocation of non-verified sequence is treated as a programmer error and leads to an assertion.
Setup sequences expectation is always evaluated, no matter if it responsible for a return value or not. Therefore, you can assign several expectations for the same function without conflicts.
let johnSequence = setupStubSequence(of: &databaseMock.addAccountAction)
.whenFirst("John")
.returns(1)
.expect(.once)
let appleseedSequence = setupStubSequence(of: &databaseMock.addAccountAction)
.whenSecond("Appleseed")
.expect(.once)
databaseMock.addUser(givenName: "John", lastName: "Appleseed") // returns 1
SKTVerify(johnSequence) // ✅
SKTVerify(appleseedSequence) // ✅
To verify that given function has been called within a range of times, just duplicate the expectations that bounds the range, e.g.:
let verify1_3 = setupStubSequence(of: &databaseMock.addUserAction)
.expect(.atLeastTimes(1))
.expect(.noMoreThan(3))
databaseMock.addUser(name: "user1")
databaseMock.addUser(name: "user2")
XCTAssert(verify1_3.verify()) // ✅
When configuring sequence by returnOnce
/throwOnce
remember that all matching sequences consume Once
behaviour:
setupStubSequence(of: &databaseMock.addAccountAction)
.returnsOnce(1)
.returnsOnce(3)
setupStubSequence(of: &databaseMock.addAccountAction)
.returnsOnce(2)
databaseMock.addUser(givenName: "John", lastName: "Appleseed") // returns 2 (and consumes 1 in a first sequence)
databaseMock.addUser(givenName: "Some", lastName: "Lastname") // returns 3
You can attach some custom callbacks to the sequence. Callbacks are executed whenever the sequence matches the predicate:
setupStubSequence(of: &databaseMock.addUserAction)
.callback {
print("Add user: \($0)")
}
setupStubSequence(of: &databaseMock.addUserAction)
.when("user2")
.callback { _ in
print("User 2 added")
}
databaseMock.addUser(name: "user1") // prints "Add user: user1"
databaseMock.addUser(name: "user2") // prints "User 2 added" and "Add user: user1"
Sequences can also fulfill the XCTestExpectation
every time function's call matches the predicate. In a tandem with XCTestExpectation.expectedFulfillmentCount
it is possible to verify number of calls
let user1Expecation = expectation(description: "Added user1")
user1Expecation.expectedFulfillmentCount = 2
setupStubSequence(of: &databaseMock.addUserAction).when("user1").callback(user1Expecation)
DispatchQueue.global(qos: .userInitiated).async {
databaseMock.addUser(name: "user1")
databaseMock.addUser(name: "user2")
databaseMock.addUser(name: "user1")
}
waitForExpectations(timeout: timeout) // ✅
By default, sequence expectation works synchronously with an explicit verification but additionally, it is possible to attach the XCTestExpectation
that gets fulfilled whenever the expectaion becomes valid:
let user1Expecation = expectation(description: "Added user1")
setupStubSequence(of: &databaseMock.addUserAction).expect(.once).callback(user1Expecation)
DispatchQueue.global(qos: .userInitiated).async {
databaseMock.addUser(name: "user2")
databaseMock.addUser(name: "user1")
}
waitForExpectations(timeout: timeout) // ✅
Support for @autoclosure in Swift 5.0 will change so this documentation is a subject to change
protocol Database {
func addUser(name: @autoclosure () -> String)
}
class DatabaseMock: Database {
lazy var addUserAction = stub(of: addUser)
func addUser(name: @autoclosure () -> String) {
return addUserAction(name())
}
}
Stubbing and spying the function with @escaping argument is really straitforward
protocol Database {
func addUser(completion: @escaping (Bool) -> ())
}
class DatabaseMock: Database {
lazy var addUserAction = stub(of: addUser)
func addUser(completion: @escaping (Bool) -> ()) {
return addUserAction(name)
}
}
Working with non-escaping functions has some limitations. Because swift opitmization assumes that closure won't be used out of a lifetime of a function's call, you have to manually guarantee that you will obey that restriction by wrapping the non-escping function call by withoutActuallyEscaping
and never save it to the storage when spying:
protocol Database {
func enumerateUsers(enumeration: (User) -> ())
}
class DatabaseMock: Database {
lazy var enumerateUsersAction = stub(of: enumerateUsers)
func enumerateUsers(enumeration: (User) -> ()) {
return withoutActuallyEscaping(f) {
enumerateUsersAction($0)
}
}
}
let enumerateUsers = spyCalls(of: &databaseStub.enumerateUsersAction, transform: { $0(User()) })
databaseMock.enumerateUsers { user in
// use user
}
let callCount = enumerateUsers.count // 1
To implement body of a protocol's function that can throw, you just need to prefix a call to an action with try
protocol Database {
func addUser(name: String) throws
}
class DatabaseMock: Database {
lazy var addUserAction = stub(of: addUser)
func addUser(name: String) throws {
return try addUserAction(name)
}
}
For argument functions that may throw, no additional keyword is needed:
protocol Database {
func addUser(completion: @escaping (Bool) throws -> ())
}
class DatabaseMock: Database {
lazy var addUserAction = stub(of: addUser)
func addUser(completion: @escaping (Bool) throws -> ()) {
return addUserAction(name)
}
}
Whenever your function is generic (do not mix up with generic types, which are really straightforward for stubbing) you cannor create a single, dedicated stub function that is called for it as your codebase can call it with any particular type. Therefore, you need to specialize your mock to handle specific generic specialization. Since swift doesn't support function specialization (e.g. let printString = print<String>
) so StubKit provides some helpers that try to hint Swift's type inference.
stubGeneric
global function builds a strictStub
(or nice stub using stub
) according to type hints provided by a developer. Once you prepared a set of specialized function actions, in a concrete generic function callGenerics
it calls the right specialization with input arguments (or crashes when specialization function is not provided).
protocol Database {
func add<User>(user: User) -> Bool
}
class DatabaseMock: Database {
lazy var addStringAction = stubGeneric(of: add).with(first:String.self).stub // type is (String) -> Bool
lazy var addIntAction = stubGeneric(of: add).with(first:Int.self).stub // type is (Int) -> Bool
func add<User>(user: User) -> Bool {
// selects right potential actions of fatalErrors in runtime when generic specialization
// not provided in potentials array
return callGeneric(user, potentials: [addStringAction, addIntAction])
}
}
DatabaseMock().add(user: "123") // calls addStringAction
DatabaseMock().add(user: 0) // calls addIntAction
Optionally, you can make your mock generic and control the expected type on a mock instance creation:
class DatabaseMock<T>: Database {
lazy var addAction = stubGeneric(of: add).with(first: T.self).stub // type is (T) -> Bool
func add<User>(user: User) -> Bool {
return callGeneric(user, potentials: [addAction])
}
}
_ = DatabaseMock<String>().add(user: "123") // calls addAction: (String) -> Bool
_ = DatabaseMock<Int>().add(user: 123) // calls addAction: (Int) -> Bool
Continue with the same approach as above, with the replacement of callGenerics
to callThrowsGeneric
:
protocol Database {
func add<User>(user: Int) throwing -> User
}
class DatabaseMock<T: DefaultProvidable>: Database {
lazy var addStringAction = stubGeneric(of: add).with(return: T.self).stub // type is (Int) -> T
func add<User>(user: Int) throws -> User {
return try callThrowsGeneric(user, potentials: [addStringAction, addIntAction])
}
}
try DatabaseMock<String>().add(user: 123) as String // calls addStringAction
try DatabaseMock<String>().add(user: 123) as Int // crashes (generic `T` and returned type `Int` mismatch)
- Thread safe - StubKit is thread unsafe
- Variadic functions