Mirage is a mocking library for swift projects. The recommended way is to use Fata Morgana for mocks generation to avoid writing them manually.
Using Mirage you can:
- create mocks, stubs, partial mocks
- verify
func
call times - get call arguments history
Requires Swift 4.2+
Add this line into your Cartfile, run carthage update --platform iOS
and link binary to the target as you always do it)
github "valnoc/Mirage" ~> 2.0
Add this line into your Podfile under a test target and run pod update
pod 'Mirage', '~> 2.0'
Podfile example
target 'MainTarget' do
...
target 'TestTarget' do
inherit! :search_paths
pod 'Mirage'
end
end
Copy /Mirage folder into your test target.
Check Example project for details. Try Fata Morgana for mocks generation.
A Mock is an object which mimics behaviour of a real object and records functions' calls. You can create class
mocks and protocol
mocks in the same way.
The first version of Mirage provided the instruments to create a mock for a whole class or a protocol. A mock could be easily created manually but the usage was not so good - you had to cast args to there types every time you call
args(of:)
and the stubs returnedAny
. Since Mirage 2 funcs are mocked individually.
All mocks and stubs are generics. They use TArgs
and TReturn
types.
If a func has one argument TArgs
should be of its type. But if it has several arguments you should create a struct (or class) as a container of this args.
TReturn
represents func's return type.
Let's create a mock for this class
class Calculator {
func sum(_ left: Int, _ right: Int) -> Int {
return left + right
}
}
- Create a new Mock class inhereted from the original
import Mirage
class MockCalculator: Calculator {
func sum
has 2 argsleft
andright
so let's create a nested class (or a struct) to contain them
class SumArgs {
let left: Int
let right: Int
init(left: Int, right: Int) {
self.left = left
self.right = right
}
}
- Add this
func
to call real implementation of this function
fileprivate func super_sum(_ args: SumArgs) -> Int {
return super.sum(args.left, args.right)
}
- Add
FuncCallHandler
. This is the core of func mocking.
lazy var mock_sum = FuncCallHandler<SumArgs, Int>(returnValue: anyInt(),
callRealFunc: { [weak self] (args) -> Int in
guard let __self = self else { return anyInt() }
return __self.super_sum(args)
})
override
the original func and callmock_sum
to handle func call
override func sum(_ left: Int, _ right: Int) -> Int {
let args = SumArgs(left: left, right: right)
return mock_sum.handle(args)
}
This is it)
class MockCalculator: Calculator {
//MARK: - sum
class SumArgs {
let left: Int
let right: Int
init(left: Int, right: Int) {
self.left = left
self.right = right
}
}
fileprivate func super_sum(_ args: SumArgs) -> Int {
return super.sum(args.left, args.right)
}
lazy var mock_sum = FuncCallHandler<SumArgs, Int>(returnValue: anyInt(),
callRealFunc: { [weak self] (args) -> Int in
guard let __self = self else { return anyInt() }
return __self.super_sum(args)
})
override func sum(_ left: Int, _ right: Int) -> Int {
let args = SumArgs(left: left, right: right)
return mock_sum.handle(args)
}
}
super_sum
and callRealFunc
can be skipped if you are not going to use it.
You can also use a struct and get a generated init
.
class MockCalculator: Calculator {
//MARK: - sum
struct SumArgs {
let left: Int
let right: Int
}
lazy var mock_sum = FuncCallHandler<SumArgs, Int>(returnValue: anyInt())
override func sum(_ left: Int, _ right: Int) -> Int {
let args = SumArgs(left: left, right: right)
return mock_sum.handle(args)
}
}
Function stubbing allows to change the behavour of a function according to testing needs.
To create a stub, call mock function whenCalled()
.
Then call one of the following functions:
thenReturn(_ result: TReturn)
to return the exact value as a resultthenDo(_ closure: @escaping Action)
to execute closure instead of called functionthenCallRealFunc()
to call real implementation of this function
This thenSmth
calls can be chained to return one result for the first call and another one for the next calls.
calculator.mock_sum.whenCalled().thenReturn(number)
randomNumberGenerator.mock_makeInt.whenCalled()
.thenReturn(5)
.thenReturn(10)
A Partial mock is the same thing as a mock but it automatically calls real implementations of its functions. There are discussions whether partial mock is a pattern or an anti-pattern, whether you therefore should use them or not.
Mirage allows you to create a partial mock with one line of code. It's up to you - to use or not to use.
To create a partial mock of a func, add isPartial: true
to a FuncCallHandler
lazy var mock_performMainOperation = FuncCallHandler<Void, Void>(returnValue: (),
isPartial: true,
callRealFunc: { [weak self] (args) -> Void in
guard let __self = self else { return () }
return __self.super_performMainOperation()
})
You can call verify(called:)
on any FuncCallHandler
to check the number of times this func was called
- never: callTimes == 0
- once: callTimes == 1
- times: callTimes == *value*
- atLeast: callTimes >= *value*
- atMost: callTimes <= *value*
verify(called:)
throws CallTimesRuleIsBroken
if actual call times do not match the given rule.
Use XCTAssertNoThrow(try ...)
with verify
call
XCTAssertNoThrow(try randomNumberGenerator.mock_makeInt.verify(called: .times(2)))
XCTAssertNoThrow(try calculator.mock_sum.verify(called: .once))
XCTAssertNoThrow(try logger.mock_logPositiveResult.verify(called: .once))
XCTAssertNoThrow(try logger.mock_logNegativeResult.verify(called: .never))
You can get arguments of any call from history using args() -> TArgs?
or args(callTime: Int) -> TArgs?
functions. It returns an array of arguments for this call or nil if no call with given callTime was registered.
So the best pratice is to use guard and XCTFail around argsOf()
if you expect this args to exist.
// then
guard let args = calculator.mock_sum.args() else { XCTFail(); return }
XCTAssert(args.left == 5)
XCTAssert(args.right == 10)
In order to migrate a project from the first version of Mirage framework to the second one, the framework has been renamed as Mirage2
. It allows to use both the first and the second versions in the same project and to migrate file by file.
Migration from Mirage 1 to Mirage 2 consists of several steps.
- Rewrite mocks
It is very easy to migrate mocks with new version of Fata Morgana
-
Use find&replace feature to change
Once()
to.once
, etc -
Use find&replace feature along with regexps to change
verify
towhen
calls I'll give regexps here later -
Remove args casts after
args()
calls
Mirage is available under MIT License.