Mimic
is a testing framework designed specifically for the Swift
programming language. It offers two key features that simplify the testing process.
- Firstly, it provides a convenient way to create fake objects, which can be used similarly as a mock object.
- Secondly, it could generate instances for codable classes.
Since Swift's
reflection is limited and rather we call it to introspection, creating runtime mocking frameworks are hardly possible. Most of the solutions are working with build time mock generations where creating mocks are marked with protocols or comments. This solution was created because we felt these solutions a bit unconvinient or not clean enough from a software development point of view.
Of course perfect solution doesn't exist, and this is also just a leaky abstraction , the trade of here is that we have to create fake classes manually but this package helps us to make this painful method as convinient as possible. Moreover to be able to replace the behaviour of a class it forces the developer to organize the code in a clean way.
By using fake objects, you can simulate various scenarios and ensure that your code behaves as expected in different situations by isolating the behaviors of specific components during testing. Mimic
provides a simple and intuitive API that allows you to specify the expected behavior and responses of the fake object. This way, you can precisely control the interactions between your tests and the fake object, making testing more reliable and reproducible.
Lets say we have a protocol which defines a function.
protocol SomeProtocol {
func someFunction(with parameter: String) -> Int
}
And we have a class which conforms to this protocol and adds some implementation for that.
class SomeClass: SomeProtocol {
func someFunction(with parameter: String) -> Int {
return 42
}
}
So now comes the tricky part, to create a fake object that could be used from tests for SomeClass
class, we should do this:
final class FakeSomeClass: SomeProtocol, Mimic {
let fnSomeFunction = Fn<Int>()
func someFunction(with parameter: String) -> Int {
return try! fnSomeFunction.invoke(params: parameter)
}
}
- add
Mimic
protocol to the fake class - create a
Fn
instance where the generic type is the result type of the function that behaviour we want to replace - add the fn instance to the function and invoke, pass the parameters and return with it
That's it. So as it is visible from the example above, it needs some code typing, but not so terrible, no need to add internal logic, or enything else, just a pure declaration, and teachings and verifications will work.
If we have a fake class that has properties, we can easily check their values by creating an expected dictionary where the key is the name of the property and the values are what we expecting, than make Props object from both and check their equality.
let expected = [
"boolValue": true,
"wholeValue": 42,
"floatingValue": 3.1415926535897,
"stringValue": "Oh, yeah. Oooh, ahhh, that's how it always starts. Then later there's running and um, screaming.",
"array": [true, false, true],
"dict": ["key": "value"],
"optionalNil": nil
].toProps()
let result = testStruct.props()
XCTAssertEqual(result, expected)
Sometimes we need more control on the function.
let expected = 496
var parameter: String!
var times = 0
fakeSomeClass.when(\.fnSomeFunction).replaceFunction { invocationCount, params in
times = invocationCount
parameter = params[0]
return expected
}
let result = testStruct.someFunction(with: "parameterValue")
XCTAssertEqual(result, expected)
XCTAssertEqual(parameter, "parameterValue")
XCTAssertEqual(times, 1)
- with calling
when
function on the faked class and passing theFn
functionskeyPath
we can use thereplaceFunction
- setting the explicit type for a parameter before getting it with index from
params
is mandatory invocationCount
can be used to check how many times were the function called
Sometimes we just want to stub it and return with a simple value.
let expected = 496
fakeSomeClass.when(\.fnSomeFunction).thenReturn(expected)
let result = testStruct.someFunction(with: "parameterValue")
XCTAssertEqual(result, expected)
- with calling
when
function on the faked class and passing theFn
functionskeyPath
we can use thethenReturn
- as a parameter of
thenReturn
function we can pass the return value
Sometimes we just want to stub it and return with a simple value.
let expected1 = 6
let expected2 = 28
let expected3 = 496
let expected5 = 8128
fakeSomeClass.when(\.fnSomeFunction).thenReturns(expected1, expected2 , expected3, nil, expected4)
let result1 = testStruct.someFunction(with: "parameterValue")
let result2 = testStruct.someFunction(with: "parameterValue")
let result3 = testStruct.someFunction(with: "parameterValue")
let result4 = testStruct.someFunction(with: "parameterValue")
let result5 = testStruct.someFunction(with: "parameterValue")
XCTAssertEqual(result1, expected1)
XCTAssertEqual(result2, expected2)
XCTAssertEqual(result3, expected3)
XCTAssertNil(result4)
XCTAssertEqual(result5, expected5)
- with calling
when
function on the faked class and passing theFn
functionskeyPath
we can use thethenReturns
- as a parameter of
thenReturns
function we can pass the return values - if we call the function more times then return values, a
MimicError.missingResult
error will be thrown
Just like in mocking frameworks, mimic
has matchers as well, it could be use for teaching and verification.
Arg.any
it could be anything without restrictions.Arg.eq(<value>)
it is an equality mathcer where the<value>
means the expected value.Arg.nil
it expects that the value will benil
.Arg.notNil
it expects that the valu won't benil
.Arg.invokeClosure(<closure>)
the givenclosure
will be called when the function invocation happens.
let expectedArgument = "testExpectedArgument"
fakeSomeClass.when(\.fnSomeFunction).calledWith(Arg.eq(expectedArgument)).thenReturn(42)
let result = testStruct.someFunction(with: expectedArgument)
XCTAssertEqual(result, 42)
- matchers can be used with
calledWith
method afterwhen
andverify
as well - after
calledWith
other functions could be called, likethenReturn
- in the example above, the
thenReturn
will be used only if the arguments passed to the funtion fits with the matchers
It is just simply throw the given Error when the function being called.
fakeSomeClass.when(\.fnSomeFunction).thenThrow(error: TestError.magicWord)
let result = testStruct.someFunction(with: "value")
This could be used to check on which thread the function was called.
try fakeSomeClass.verify(\.fnSomeFunction).on(thread: Thread.current)
This could be used to check how many times the function was called.
try fakeSomeClass.verify(\.fnSomeFunction).times(times: .atLeast(2))
Possible values:
.zero
it means the function should not be called.atLeast(<Int value>)
it means the function should be called<Int value>
or more times.atMax(<Int value>)
it means the function should be called maximum<Int value>
times.eq(<Int value>)
it means the function should be called exactly<Int value>
times
When working with data classes, it's often necessary to create instances of them for testing purposes. Mimic simplifies this process by automatically generating instances with random values. This feature becomes especially handy when dealing with complex data models or when you need to generate a large number of instances for testing different scenarios. With this feature you can save time and effort that you could spend instead for the actual testing.
To use this feature you just simply add @Generate
attribute before your variable.
@Generate
var someStruct: SomeStruct
That's it, after this code you could simply use the someStruct variable.
Limitations:
- the class itself should be implement the
Decodable
protocol - if the class contains enums or other complex properties, those have to implement the
Codable
protocol and an instance should be passed, to theGenerate
attribute.
@Generate(SomeEnum.case1(42))
var someStruct: SomeStruct
This team is the maker of the EmarsysSDK's when we started to develop the swift based version of it, we shortly arrived that point when we needed a proper mocking solution. We tried out few, but non of them was fit perfectly to our needs mainly because of those points what are highlighted above. This package and it's features are actively used in our SDK. Our team strongly beliefs that the good things, findings, solutions, knowledge should be shared that's why we made this repo publicly available to anyone. We hope you will consider it useful and will help your work.
Disclaimer
As we mentioned before, at first place this package was created for internal use only. The main goal was to cover all of our use cases and fulfill our requirements what doesn't mean that this is good for everyone. If you find lack of features or issues we do not say that those will be implemented or solved in the near future since this product is not actively developed. We do our best, but no promises.
The logo was created by Bing Image Creator